برای شروع ابتدا مدل برنامه رو به صورت زیر تعریف کنید.
public class Category { public int Id { get; set; } public string Title { get; set; } }
public class CategoryMap : EntityTypeConfiguration<Entity.Category> { public CategoryMap() { ToTable( "Category" ); HasKey( _field => _field.Id ); Property( _field => _field.Title ) .IsRequired(); } }
using System.Data.Entity; using System.Data.Entity.Infrastructure; namespace DataAccess { public interface IUnitOfWork { DbSet<TEntity> Set<TEntity>() where TEntity : class; DbEntityEntry<TEntity> Entry<TEntity>() where TEntity : class; void SaveChanges(); void Dispose(); } }
چون کلاس DatabaseContext از اینترفیس IUnitOfWork ارث برده است برای همین از InheritedExport استفاده میکنیم.
[InheritedExport( typeof( IUnitOfWork ) )] public class DatabaseContext : DbContext, IUnitOfWork { private DbTransaction transaction = null; public DatabaseContext() { this.Configuration.AutoDetectChangesEnabled = false; this.Configuration.LazyLoadingEnabled = true; } protected override void OnModelCreating( DbModelBuilder modelBuilder ) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); modelBuilder.AddFormAssembly( Assembly.GetAssembly( typeof( Entity.Map.CategoryMap ) ) ); } public DbEntityEntry<TEntity> Entry<TEntity>() where TEntity : class { return this.Entry<TEntity>(); } }
public static class ModelBuilderExtension { public static void AddFormAssembly( this DbModelBuilder modelBuilder, Assembly assembly ) { Array.ForEach<Type>( assembly.GetTypes().Where( type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof( EntityTypeConfiguration<> ) ).ToArray(), delegate( Type type ) { dynamic instance = Activator.CreateInstance( type ); modelBuilder.Configurations.Add( instance ); } ); } }
برای پیاده سازی قسمت BusinessLogic ابتدا کلاس BusiessBase را در آن قرار دهید:
public class BusinessBase<TEntity> where TEntity : class { public BusinessBase( IUnitOfWork unitOfWork ) { this.UnitOfWork = unitOfWork; } [Import] public IUnitOfWork UnitOfWork { get; private set; } public virtual IEnumerable<TEntity> GetAll() { return UnitOfWork.Set<TEntity>().AsNoTracking(); } public virtual void Add( TEntity entity ) { try { UnitOfWork.Set<TEntity>().Add( entity ); UnitOfWork.SaveChanges(); } catch { throw; } finally { UnitOfWork.Dispose(); } } }
تمام متدهای پایه مورد نظر را باید در این کلاس قرار داد که برای مثال من متد Add , GetAll را براتون پیاده سازی کردم. UnitOfWork توسط ImportAttribute مقدار دهی میشود و نیاز به وهله سازی از آن نیست
کلاس Category رو هم باید به صورت زیر اضافه کنید.
public class Category : BusinessBase<Entity.Category> { [ImportingConstructor] public Category( [Import( typeof( IUnitOfWork ) )] IUnitOfWork unitOfWork ) : base( unitOfWork ) { } }
public class Plugin { public void Run() { AggregateCatalog catalog = new AggregateCatalog(); Container = new CompositionContainer( catalog ); CompositionBatch batch = new CompositionBatch(); catalog.Catalogs.Add( new AssemblyCatalog( Assembly.GetExecutingAssembly() ) ); batch.AddPart( this ); Container.Compose( batch ); } public CompositionContainer Container { get; private set; } }
- AssemblyCatalog : در اسمبلی مورد نظر به دنبال تمام Export Attributeها میگردد و آنها را به عنوان ExportedValue در Container اضافه میکند.
- TypeCatalog: فقط یک نوع مشخص را به عنوان ExportAttribute در نظر میگیرد.
- DirectoryCatalog : در یک مسیر مشخص تمام Assembly مورد نظر را از نظر Export Attribute جستجو میکند و آنها را به عنوان ExportedValue در Container اضافه میکند.
- ApplicationCatalog : در اسمبلی و فایلهای (EXE) مورد نظر به دنبال تمام Export Attributeها میگردد و آنها را به عنوان ExportedValue در Container اضافه میکند.
- AggregateCatalog : تمام موارد فوق را Support میکند.
class Program { static void Main( string[] args ) { Plugin plugin = new Plugin(); plugin.Run(); Category category = new Category(plugin.Container.GetExportedValue<IUnitOfWork>()); category.GetAll().ToList().ForEach( _record => Console.Write( _record.Title ) ); } }
عموما دو الگوی اصلی caching در برنامهها وجود دارند: cache aside و cache trough .
در الگوی cache trough ، سیستم caching داخل DAL (که در اینجا همان NHibernate است)، تعبیه میشود؛ مانند سطح اول caching که پیشتر در مورد آن صحبت شد. در این حالت cache از دید سایر قسمتهای برنامه مخفی است و DAL به صورت خودکار آنرا مدیریت میکند.
در الگوی cache aside ، کار مدیریت سیستم caching دستی است و خارج از NHibernate قرار میگیرد و DAL هیچگونه اطلاعی از وجود آن ندارد. در این حالت لایه caching موظف است تا هنگام به روز شدن بانک اطلاعاتی، اطلاعات خود را نیز به روز نماید. این لایه عموما توسط سایر شرکتها یا گروهها برنامه نویسی تهیه میشود.
NHibernate جهت سهولت کار با این نوع cache providers خارجی، نقاط تزریق ویژهای را تدارک دیده است که به second level cache معروف است. هدف از second level cache فراهم آوردن دیدی کش شده از بانک اطلاعاتی است تا فراخوانیهای کوئریها به سرعت و بدون تماس با بانک اطلاعاتی صورت گیرد.
در حال حاضر (زمان نگارش این مطلب)، entity framework این لایهی دوم caching یا به عبارتی دیگر، امکان تزریق سادهتر cache providers خارجی را به صورت توکار ارائه نمیدهد.
در NHibernate طول عمر second level cache در سطح session factory (یا به عبارتی طول عمر تمام برنامه) تعریف میشود و برخلاف سطح اولیه caching محدود به یک سشن نیست. در این حالت هر زمانیکه یک موجودیت به همراه ID منحصربفرد آن تحت نظر NHibernate قرارگیرد و همچنین سطح دوم caching نیز فعال باشد، این موجودیت در تمام سشنهای برنامه بدون نیاز به مراجعه به بانک اطلاعاتی در دسترس خواهد بود (بنابراین باید دقت داشت که هدف از این سیستم، کار سریعتر با اطلاعاتی است که سطح دسترسی عمومی دارند).
در ادامه لیستی از cache providers خارجی مهیا جهت استفاده در سطح دوم caching را ملاحظه مینمائید:
- AppFabric Caching Services : بر اساس Microsoft's AppFabric Caching Services که یک پلتفرم caching محسوب میشود (+). (این پروژه پیشتر به نام Velocity معروف شده بود و قرار بود تنها برای ASP.NET ارائه شود که سیاست آن به گونهای جامعتر تغییر کرده است)
- MemCache : بر اساس سیستم معروف MemCached تهیه شده است (+).
- NCache : (+)
- ScaleOut : (+)
- Prevalence : (+)
- SysCache : بر اساس همان روش آشنای متداول در برنامههای ASP.NET به کمک System.Web.Caching.Cache کار میکند؛ یا به قولی همان IIS caching
- SysCache2 : همانند SysCache است با این تفاوت که SQL dependencies ویژه SQL Server را نیز پشتیبانی میکند.
- SharedCache : یک سیستم distributed caching نوشته شده برای دات نت است (+).
مطالب تکمیلی:
موجودیتهای زیر را در نظر بگیرید:
public class Customer { public Customer() { Orders = new ObservableCollection<Order>(); } public Guid Id { get; set; } public string Name { get; set; } public string Family { get; set; } public string FullName { get { return Name + " " + Family; } } public virtual IList<Order> Orders { get; set; } }
public class Product { public Product() { } public Guid Id { get; set; } public string Name { get; set; } public int Price { get; set; } } public class OrderDetail { public Guid Id { get; set; } public Guid ProductId { get; set; } public int Count { get; set; } public Guid OrderId { get; set; } public int Price { get; set; } public virtual Order Order { get; set; } public virtual Product Product { get; set; } public string ProductName { get { return Product != null ? Product.Name : string.Empty; } } }
public class Order { public Order() { OrderDetail = new ObservableCollection<OrderDetail>(); } public Guid Id { get; set; } public DateTime Date { get; set; } public Guid CustomerId { get; set; } public virtual Customer Customer { get; set; } public virtual IList<OrderDetail> OrderDetail { get; set; } public string CustomerFullName { get { return Customer == null ? string.Empty : Customer.FullName; } } public int TotalPrice { get { if (OrderDetail == null) return 0; return OrderDetail.Where(orderdetail => orderdetail.Product != null) .Sum(orderdetail => orderdetail.Price*orderdetail.Count); } } }
و نگاشت موجودیت ها:
public class CustomerConfiguration : EntityTypeConfiguration<Customer> { public CustomerConfiguration() { HasKey(c => c.Id); Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } } public class ProductConfiguration : EntityTypeConfiguration<Product> { public ProductConfiguration() { HasKey(p => p.Id); Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } } public class OrderDetailConfiguration : EntityTypeConfiguration<OrderDetail> { public OrderDetailConfiguration() { HasKey(od => od.Id); Property(od => od.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } } public class OrderConfiguration: EntityTypeConfiguration<Order> { public OrderConfiguration() { HasKey(o => o.Id); Property(o => o.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } }
و برای معرفی موجودیتها به Entity Framwork کلاس StoreDbContext را به صورت زیر تعریف میکنیم:
public class StoreDbContext : DbContext { public StoreDbContext() : base("name=StoreDb") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new CustomerConfiguration()); modelBuilder.Configurations.Add(new OrderConfiguration()); modelBuilder.Configurations.Add(new OrderDetailConfiguration()); modelBuilder.Configurations.Add(new ProductConfiguration()); } public DbSet<Customer> Customers { get; set; } public DbSet<Product> Products { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderDetail> OrderDetails { get; set; } }
جهت مقدار دهی اولیه به database تستی یک DataBaseInitializer به صورت زیر تعریف میکنیم:
public class MyTestDb : DropCreateDatabaseAlways<StoreDbContext> { protected override void Seed(StoreDbContext context) { var customer1 = new Customer { Name = "Vahid", Family = "Nasiri" }; var customer2 = new Customer { Name = "Mohsen", Family = "Jamshidi" }; var customer3 = new Customer { Name = "Mohsen", Family = "Akbari" }; var product1 = new Product {Name = "CPU", Price = 350000}; var product2 = new Product {Name = "Monitor", Price = 500000}; var product3 = new Product {Name = "Keyboard", Price = 30000}; var product4 = new Product {Name = "Mouse", Price = 20000}; var product5 = new Product {Name = "Power", Price = 70000}; var product6 = new Product {Name = "Hard", Price = 250000}; var order1 = new Order { Customer = customer1, Date = new DateTime(2013, 1, 1), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product1, Count = 1, Price = product1.Price}, new OrderDetail {Product = product2, Count = 1, Price = product2.Price}, new OrderDetail {Product = product3, Count = 1, Price = product3.Price}, } }; var order2 = new Order { Customer = customer1, Date = new DateTime(2013, 1, 5), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product1, Count = 2, Price = product1.Price}, new OrderDetail {Product = product3, Count = 4, Price = product3.Price}, } }; var order3 = new Order { Customer = customer1, Date = new DateTime(2013, 1, 9), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product1, Count = 4, Price = product1.Price}, new OrderDetail {Product = product3, Count = 5, Price = product3.Price}, new OrderDetail {Product = product5, Count = 6, Price = product5.Price}, } }; var order4 = new Order { Customer = customer2, Date = new DateTime(2013, 1, 9), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product4, Count = 1, Price = product4.Price}, new OrderDetail {Product = product3, Count = 1, Price = product3.Price}, new OrderDetail {Product = product6, Count = 1, Price = product6.Price}, } }; var order5 = new Order { Customer = customer2, Date = new DateTime(2013, 1, 12), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product4, Count = 1, Price = product4.Price}, new OrderDetail {Product = product5, Count = 2, Price = product5.Price}, new OrderDetail {Product = product6, Count = 5, Price = product6.Price}, } }; context.Customers.Add(customer3); context.Orders.Add(order1); context.Orders.Add(order2); context.Orders.Add(order3); context.Orders.Add(order4); context.Orders.Add(order5); context.SaveChanges(); }
و در ابتدای برنامه کد زیر را جهت مقداردهی اولیه به Database مان قرار میدهیم:
Database.SetInitializer(new MyTestDb());
در انتها ConnectionString را در App.Config به صورت زیر تعریف میکنیم:
<connectionStrings> <add name="StoreDb" connectionString="Data Source=.\SQLEXPRESS; Initial Catalog=StoreDBTest;Integrated Security = true" providerName="System.Data.SqlClient"/> </connectionStrings>
بسیار خوب، حالا همه چیز محیاست برای اجرای اولین پرس و جو:
using (var context = new StoreDbContext()) { var query = context.Customers; foreach (var customer in query) { Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); } }
پرس و جوی تعریف شده لیست تمام Customerها را باز میگرداند. query فقط یک "عبارت" پرس و جو هست و زمانی اجرا میشود که از آن درخواست نتیجه شود. در مثال بالا این درخواست در اجرای حلقه foreach اتفاق میافتد و درست در این لحظه است که دستور SQL ساخته شده و به Database فرستاده میشود. EF در این حالت تمام دادهها را در یک لحظه باز نمیگرداند بلکه این ارتباط فعال است تا حلقه به پایان برسد و تمام دادهها از database واکشی شود. خروجی به صورت زیر خواهد بود:
Customer Name: Vahid, Customer Family: Nasiri Customer Name: Mohsen, Customer Family: Jamshidi Customer Name: Mohsen, Customer Family: Akbari
نکته: با هر بار درخواست نتیجه از query ، پرس و جوی مربوطه دوباره به database فرستاده میشود که ممکن است مطلوب ما نباشد و باعث افت سرعت شود. برای جلوگیری از تکرار این عمل کافیست با استفاده از متد ToList پرس و جو را در لحظه تعریف به اجرا در آوریم
var customers = context.Customers.ToList();
خط بالا دیگر یک عبارت پرس و جو نخواهد بود بلکه لیست تمام Customer هاست که به یکباره از database بازگشت داده شده است. در ادامه هرجا که از customers استفاده کنیم دیگر پرس و جویی به database فرستاده نخواهد شد.
پرس و جوی زیر مشتریهایی که نام آنها Mohsen هست را باز میگرداند:
private static void Query3() { using (var context = new StoreDbContext()) { var methodSyntaxquery = context.Customers .Where(c => c.Name == "Mohsen"); var sqlSyntaxquery = from c in context.Customers where c.Name == "Mohsen" select c; foreach (var customer in methodSyntaxquery) { Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); } } // Output: // Customer Name: Mohsen, Customer Family: Jamshidi // Customer Name: Mohsen, Customer Family: Akbari }
همانطور که مشاهده میکنید پرس و جو به دو روش Method Syntax و Sql Syntax نوشته شده است.
روش Method Syntax روشی است که از متدهای الحاقی (Extention Method) و عبارتهای لامبدا (Lambda Expersion) برای نوشتن پرس و جو استفاده میشود. اما #C روش Sql Syntax را که همانند دستورات SQL هست، نیز فراهم کرده است تا کسانیکه آشنایی با این روش دارند، از این روش استفاده کنند. در نهایت این روش به Method Syntax تبدیل خواهد شد بنابراین پیشنهاد میشود که از همین روش استفاده شود تا با دست و پنجه نرم کردن با این روش، از مزایای آن در بخشهای دیگر کدنویسی استفاده شود.
اگر به نوع Customers که در DbContext تعریف شده است، دقت کرده باشید، خواهید دید که DbSet میباشد. DbSet کلاس و اینترفیسهای متفاوتی را پیاده سازی کرده است که در ادامه با آنها آشنا خواهیم شد:
- IQueryable<TEntity>, IEnumerable<TEntity>, IQueryable, IEnumerable: که امکان استفاده از متدهای نام آشنای LINQ را برای ما فراهم میکند. البته فراموش نشود که EF از Provider ای با نام LINQ To Entity برای تفسیر پرس و جوی ما و ساخت دستور SQL متناظر آن استفاده میکند. بنابراین تمامی متدهایی که در LINQ To Object استفاده میشوند در اینجا قابل استفاده نیستند. بطور مثال اگر در پرس و جو از LastOrDefault روی Customer استفاده شود در زمان اجرا با خطای زیر مواجه خواهیم شد و در نتیجه در استفاده از این متدها به این مسئله باید دقت شود.
- <IDbSet<TEntity: که دارای متدهای Add, Attach, Create, Find, Remove, Local میباشد و برای بحث ما Find و Local جهت ساخت پرس و جو استفاده میشوند که در ادامه توضیح داده خواهند شد.
- <DbQuery<TEntity: که دارای متدهای AsNoTracking و Include میباشد و در ادامه توضیح داده خواهند شد.
- دادههای موجود در حافظه را بررسی میکند یعنی آنهایی که Load و یا Attach شده اند.
- داده هایی که به DbContext اضافه (Add) ولی هنوز در database درج نشده اند.
- داده هایی که در database هستند ولی هنوز Load نشده اند.
private static void Query4() { using (var context = new StoreDbContext()) { var customer = context.Customers.Find(new Guid("2ee2fd32-e0e9-4955-bace-1995839d4367")); if (customer == null) Console.WriteLine("Customer not found"); else Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); } }
private static void Query5() { using (var context = new StoreDbContext()) { try { var customer1 = context.Customers.Single(c => c.Name == "Unkown"); // Exception: Sequence contains no elements } catch (Exception ex) { Console.WriteLine(ex.Message); } try { var customer2 = context.Customers.Single(c => c.Name == "Mohsen"); // Exception: Sequence contains more than one element } catch (Exception ex) { Console.WriteLine(ex.Message); } var customer3 = context.Customers.SingleOrDefault(c => c.Name == "Unkown"); // customer3 == null var customer4 = context.Customers.Single(c => c.Name == "Vahid"); // customer4 != null } }
private static void Query6() { using (var context = new StoreDbContext()) { try { var customer1 = context.Customers.First(c => c.Name == "Unkown"); // Exception: Sequence contains no elements } catch (Exception ex) { Console.WriteLine(ex.Message); } var customer2 = context.Customers.FirstOrDefault(c => c.Name == "Unknown"); // customer2 == null var customer3 = context.Customers.First(c => c.Name == "Mohsen"); } }
دو تنظیم زیر را در نظر بگیرید:
<add key="nhibernate-logger" value="NHibernate.Helper.Logging.LoggerFactory, NHibernate.Helper" />
<add name="StaticContentCacheModule" type="StaticContentCacheModule.StaticCache, StaticContentCacheModule"/>
الان فرض کنید کلاس StaticCache مربوط به StaticContentCacheModule فرضی فوق را به صورت دستی به برنامهی خود اضافه کردهاید. همچنین سطر فوق را نیز بدون هیچ تغییری در قسمت http modules مربوط به web.config برنامه معرفی نمودهاید. برنامه را اجرا میکنید، اما ماژول ذکر شده کار نمیکند! چرا؟
چون نام رشتهای متناظر با کلاس StaticCache ایی که اکنون به پروژهی خود اضافه کردهاید، با توجه به فضاهای نام پروژهی جدید، کاملا دگرگون شده است. بنابراین، سؤال مهم اینجا است که این نام را بر اساس تنظیمات پروژهی جاری چگونه میتوان یافت؟
خوشبختانه دات نت فریم ورک، ابزاری توکار را برای تولید این نام رشتهای، به همراه دارد:
class Test
{
static void Main()
{
string name = typeof(System.Data.DataView).AssemblyQualifiedName;
Console.WriteLine(name);
}
}
خروجی این مثال جهت نمایش نام رشتهای معادل کلاس System.Data.DataView به صورت زیر است:
System.Data.DataView, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
سؤال: از کجا متوجه شوم که رشتهی فوق واقعا کار میکند؟
مقدار متغیر name مثال فوق باید پس از بکارگیری در متد Type.GetType ، حاصلی غیر null را بازگشت دهد.
var name = typeof(System.Data.DataView).AssemblyQualifiedName;
var type = Type.GetType(name);
نکته: اگر قصد معرفی اسمبلی دیگری را به برنامه دارید و این اسمبلی امضای دیجیتال دارد (strong name signature)، باید تمام اطلاعات حاصل را ذکر کنید (مانند مثال فوق که شامل Version ، Public key token و غیره است). در غیر اینصورت (عدم وجود امضای دیجیتال) ذکر دو قسمت اول خروجی خاصیت AssemblyQualifiedName کافی خواهند بود.
اس کیوال سرور
توسعه وب
دات نت فریم ورک
شیرپوینت
متفرقه
محیطهای مجتمع توسعه
مسایل انسانی، اجتماعی و مدیریتی برنامه نویسی
ویندوز
تعیین اجباری بودن یا نبودن ستونها در EF Core
به صورت پیش فرض در EF Core، هر نوع CLR ایی که نال پذیر باشد، به صورت یک ستون اختیاری در نظر گرفته میشود؛ مانند:
string, int?, byte[]
int, decimal, bool, DateTime
برای لغو اختیاری بودن یک خاصیت نال پذیر میتوان از ویژگی Required استفاده کرد:
[Required] public string Url { get; set; }
و یا معادل Fluent API آن با استفاده از ذکر متد IsRequired است:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Url) .IsRequired(); }
کار با رشتهها در EF Core
ذکر یک خاصیت رشتهای به این صورت:
public string FirstName { get; set; }
[StringLength(450)] public string FirstName { get; set; } [MaxLength(450)] public string LastName { get; set; } [MaxLength] public string Address { get; set; }
برای تعیین صریح یک فیلد رشتهای به حداکثر مقدار آن بهتر است ویژگی MaxLength را بدون ذکر پارامتری قید کرد. این مورد جهت سازگاری با بانکهای اطلاعاتی مختلف ضروری است.
معادل این تنظیمات با روش Fluent API به صورت زیر است:
برای تعیین صریح طول یک فیلد رشتهای:
modelBuilder.Entity<Person>() .Property(x => x.Address) .HasMaxLength(450);
modelBuilder.Entity<Person>() .Property(x => x.Address) .HasColumnType("nvarchar(max)");
[Column(TypeName = "varchar")] [MaxLength] public string Address { get; set; }
نکتهای در مورد تغییر نوع خواص: اگر از متد HasColumnType و یا ویژگی Column به نحو فوق استفاده کردید، نیاز است طول رشته را صریحا مشخص کنید. در غیر اینصورت در حین migration خطای ذیل را دریافت خواهید کرد:
Data type 'varchar' is not supported in this form. Either specify the length explicitly in the type name, for example as 'varchar(16)', or remove the data type and use APIs such as HasMaxLength to allow EF choose the data type.
[Column(TypeName = "varchar(max)")]
نکتهای در مورد ایندکسها: در قسمت قبل عنوان شد که میتوان بر روی خواص، ایندکس منحصربفرد اعمال کرد. در مورد رشتهها در SQL Server، اگر طول فیلد مدنظر حداکثر تا 900 بایت باشد، یک چنین کاری را میتوان انجام داد. البته این محدودیت 900 بایتی تا SQL Server 2014 وجود دارد. این سقف در SQL Server 2016 به 1700 بایت افزایش یافتهاست (900bytes for a clustered index. 1,700 for a nonclustered index). بنابراین چون نوع پیش فرض ستونهای رشتهای، یونیکد و nvarchar درنظر گرفته میشود، حداکثر طول امنی را که میتوان برای آن تعریف کرد، مساوی 450 است (نصف 900 بایت). به همین جهت ذکر ایندکس منحصربفرد بر روی یک ستون رشتهای، باید به همراه ذکر اجباری حداکثر طول مساوی 450 آن باشد.
کار با اعداد در EF Core
کلاس نمونهای را با ساختار ذیل درنظر بگیرید:
public class Person { public int Id { set; get; } public DateTime? DateAdded { set; get; } public DateTime? DateUpdated { set; get; } [StringLength(450)] public string FirstName { get; set; } [MaxLength(450)] public string LastName { get; set; } //[Column(TypeName = "varchar")] [MaxLength] public string Address { get; set; } //bit public bool IsActive { get; set; } //tiny Int public byte Age { get; set; } //small Int public short Short { get; set; } //int public int Int32 { get; set; } //Big int public long Long { get; set; } }
همانطور که ملاحظه میکنید، نوع bool دات نت به نوع bit در SQL Server، نوع long به bigint، نوع short به smallint، نوع int به int و نوع byte به tinyint نگاشت شدهاند.
نکتهای در مورد اعداد اعشاری: توصیه شدهاست در تعاریف موجودیتهای خود بهتر است از نوعهای float و یا double استفاده نکنید. برای کار با اعداد اعشاری از نوع decimal استفاده نمائید تا بتوانید از قابلیت مقایسهی دقیق آنها استفاده کنید. اطلاعات بیشتر: «روش صحیح مقایسه دو عدد اعشاری با هم»
کار با تاریخ در EF Core
اگر به تصویر فوق دقت کنید، نوع DateTime دات نت به datetime2 در سمت SQL Server ترجمه شدهاست:
CREATE TABLE [dbo].[Persons]( [DateAdded] [datetime2](7) NULL, [DateUpdated] [datetime2](7) NULL,
اطلاعات بیشتر: «کنترل نوعهای داده با استفاده از EF در SQL Server»
به علاوه در دات نت نوع DateTime از نوع value type است. بنابراین همانطور که در ابتدای بحث نیز عنوان شد، مقدار دهی آن اجباری است؛ مگر آنکه آنرا نال پذیر تعریف کنید.
کار با مباحث همزمانی در EF Core
EF Core به صورت پیش فرض، فرض میکند رکوردی را که با آن در حال کار هستید، توسط هیچ کاربر دیگری در شبکه تغییر نیافتهاست و تغییرات شما را در حین فراخوانی متد SaveChanges ذخیره میکند. اگر علاقمند هستید که EF Core در صورت تغییر مقدار خاصیت خاصی توسط سایر کاربران، این مساله را با صدور استثنایی به شما اطلاع رسانی کند، از ویژگی ConcurrencyCheck
[ConcurrencyCheck] public string Name { set; get; }
modelBuilder.Entity<Person>() .Property(p => p.Name) .IsConcurrencyToken();
اگر علاقمند هستید که تمام فیلدهای جدول تحت نظر قرارگیرند، میتوان از روش ویژهای به نام Timestamp/row version استفاده کرد:
[Timestamp] public byte[] Timestamp { get; set; }
modelBuilder.Entity<Blog>() .Property(p => p.Timestamp) .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken();
در این حالت در حین به روز رسانی یک چنین رکوردی، اگر از زمان کوئری آن (یافتن رکورد) و ذخیره سازی آن، شخص دیگری آنرا تغییر داده باشد، به علت عدم تطابق Timestamp ها، عملیات به روز رسانی باشکست روبرو شده و یک استثناء صادر میشود.
آشنایی با NUnit
NUnit یکی از فریم ورکهای آزمایش واحد سورس باز مخصوص دات نت فریم ورک است. (کلا در دات نت هرجایی دیدید که N ، به ابتدای برنامهای یا کتابخانهای اضافه شده یعنی نمونه منتقل شده از محیط جاوا به دات نت است. برای مثال NHibernate از Hibernate جاوا گرفته شده است و الی آخر)
این برنامه با سی شارپ نوشته شده است اما تمامی زبانهای دات نتی را پشتیبانی میکند (اساسا با زبان نوشته شده کاری ندارد و فایل اسمبلی برنامه را آنالیز میکند. بنابراین فرقی نمیکند که در اینجا چه زبانی بکار گرفته شده است).
ابتدا NUnit را دریافت نمائید:
http://nunit.org/index.php?p=download
یک برنامه ساده از نوع console را در VS.net آغاز کنید.
کلاس MyList را با محتوای زیر به پروژه اضافه کنید:
using System.Collections.Generic;
namespace sample
{
public class MyList
{
public static List<int> GetListOfIntItems(int numberOfItems)
{
List<int> res = new List<int>();
for (int i = 0; i < numberOfItems; i++)
res.Add(i);
return res;
}
}
}
اکنون بر روی نام پروژه در قسمت solution explorer کلیک راست کرده و گزینه add->new project را انتخاب کنید. نوع این پروژه را که متدهای آزمایش واحد ما را تشکیل خواهد داد، class library انتخاب کنید. با نام مثلا TestLibrary (شکل زیر).
با توجه به اینکه NUnit ، اسمبلی برنامه (فایل exe یا dll آنرا) آنالیز میکند، بنابراین میتوان پروژه تست را جدای از پروژه اصلی ایجاد نمود و مورد استفاده قرار داد.
پس از ایجاد پروژه class library ، باید ارجاعی از NUnit framework را به آن اضافه کنیم. به محل نصب NUnit مراجعه کرده (پوشه bin آن) و ارجاعی به فایل nunit.framework.dll را به پروژه اضافه نمائید (شکل زیر).
سپس فضاهای نام مربوطه را به کلاس آزمایش واحد خود اضافه خواهیم کرد:
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
سپس باید ویژگی جدیدی به نام TestFixture را به این کلاس اضافه کرد.
[TestFixture]
public class TestClass
سپس هر متدی که به عنوان متد آزمایش واحد نوشته میشود، باید دارای ویژگی Test باشد تا توسط NUnit بررسی گردد:
[Test]
public void TestGetListOfIntItems()
اکنون برای اینکه بتوانیم متد GetListOfIntItems برنامه خود را در پروژه دیگری تست کنیم، باید ارجاعی را به اسمبلی آن اضافه کنیم. همانند قبل، از منوی project گزینه add reference ، فایل exe برنامه کنسول خود را انتخاب کرده و ارجاعی از آنرا به پروژه class library اضافه میکنیم. بدیهی است امکان اینکه کلاس تست در همان پروژه هم قرار میگرفت وجود داشت و صرفا جهت جداسازی آزمایش از برنامه اصلی اینکار صورت گرفت.
پس از این مقدمات، اکنون متد آزمایش واحد ساده زیر را در نظر بگیرید:
[Test]
public void TestGetListOfIntItems()
{
const int count = 5;
List<int> items = sample.MyList.GetListOfIntItems(count);
Assert.That(items.Count,Is.EqualTo(5));
}
اگر آن را (سطر مربوط به Assert را) کلمه به کلمه بخواهیم به فارسی ترجمه کنیم به صورت زیر خواهد بود:
میخواهیم اثبات کنیم که count مربوط به شیء items مساوی 5 است.
پس از اضافه کردن متد فوق، پروژه را کامپایل نمائید.
اکنون برنامه nunit.exe را اجرا کنید تا NUnit IDE ظاهر شود (در همان دایرکتوری bin مسیر نصب NUnit قرار دارد).
از منوی File آن یک پروژه جدید را آغاز نموده و آنرا ذخیره کنید.
سپس از منوی project آن، با استفاده از گزینه add assembly ، فایل dll کتابخانه تست خود را اضافه نمائید.
احتمالا پس از انجام این عملیات بلافاصله با خطای زیر مواجه خواهید شد:
---------------------------
Assembly Not Loaded
---------------------------
System.ApplicationException : Unable to load TestLibrary because it is not located under
the AppBase
----> System.IO.FileNotFoundException : Could not load file or assembly
'TestLibrary' or one of its dependencies. The system cannot find the file specified.
For further information, use the Exception Details menu item.
همانطور که ملاحظه میکنید، NUnit با استفاده از قابلیتهای reflection در دات نت، اسمبلی را بارگذاری میکند و تمامی کلاسهایی که دارای ویژگی TestFixture باشند در آن لیست خواهد شد.
اکنون بر روی دکمه run کلیک کنید تا اولین آزمایش ما انجام شود. (شکل زیر)
رنگ سبز در اینجا به معنای با موفقیت انجام شدن آزمایش است.
ادامه دارد...
ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب آن را MVC + Web API انتخاب کنید.
ابتدا به فایل WebApiConfig.cs در پوشه App_Start مراجعه کنید و مسیر پیش فرض را حذف کنید. برای مسیریابی سرویسها از قابلیت جدید Attribute Routing استفاده خواهیم کرد. فایل مذکور باید مانند لیست زیر باشد.
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services // Web API routes config.MapHttpAttributeRoutes(); } }
سپس در پوشه Models کلاس جدیدی بنام VideoStream ایجاد کنید. این کلاس مسئول نوشتن داده فایلهای ویدیویی در OutputStream خواهد بود. کد کامل این کلاس را در لیست زیر مشاهده میکنید.
public class VideoStream { private readonly string _filename; private long _contentLength; public long FileLength { get { return _contentLength; } } public VideoStream(string videoPath) { _filename = videoPath; using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read)) { _contentLength = video.Length; } } public async void WriteToStream(Stream outputStream, HttpContent content, TransportContext context) { try { var buffer = new byte[65536]; using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read)) { var length = (int)video.Length; var bytesRead = 1; while (length > 0 && bytesRead > 0) { bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length)); await outputStream.WriteAsync(buffer, 0, bytesRead); length -= bytesRead; } } } catch (HttpException) { return; } finally { outputStream.Close(); } } }
شرح کلاس VideoStream
این کلاس ابتدا دو فیلد خصوصی تعریف میکند. یکی filename_ که فقط-خواندنی است و نام فایل ویدیو درخواستی را نگهداری میکند. و دیگری contentLength_ که سایز فایل ویدیو درخواستی را نگهداری میکند.
یک خاصیت عمومی بنام FileLength نیز تعریف شده که مقدار خاصیت contentLength_ را بر میگرداند.
متد سازنده این کلاس پارامتری از نوع رشته بنام videoPath را میپذیرد که مسیر کامل فایل ویدیوی مورد نظر است. در این متد، متغیرهای filename_ و contentLength_ مقدار دهی میشوند. نکتهی قابل توجه در این متد استفاده از پارامتر FileShare.Read است که باعث میشود فایل مورد نظر هنگام باز شدن قفل نشود و برای پروسههای دیگر قابل دسترسی باشد.
در آخر متد WriteToStream را داریم که مسئول نوشتن داده فایلها به OutputStream است. اول از همه دقت کنید که این متد از کلمه کلیدی async استفاده میکند بنابراین بصورت asynchronous اجرا خواهد شد. در بدنه این متد متغیری بنام buffer داریم که یک آرایه بایت با سایز 64KB را تعریف میکند. به بیان دیگر اطلاعات فایلها را در پکیجهای 64 کیلوبایتی برای کلاینت ارسال خواهیم کرد. در ادامه فایل مورد نظر را باز میکنیم (مجددا با استفاده از FileShare.Read) و شروع به خواندن اطلاعات آن میکنیم. هر 64 کیلوبایت خوانده شده بصورت async در جریان خروجی نوشته میشود و تا هنگامی که به آخر فایل نرسیده ایم این روند ادامه پیدا میکند.
while (length > 0 && bytesRead > 0) { bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length)); await outputStream.WriteAsync(buffer, 0, bytesRead); length -= bytesRead; }
حال که کلاس VideoStream را در اختیار داریم میتوانیم پروژه را تکمیل کنیم. در پوشه کنترلرها کلاسی بنام VideoControllerبسازید. کد کامل این کلاس را در لیست زیر مشاهده میکنید.
public class VideoController : ApiController { [Route("api/video/{ext}/{fileName}")] public HttpResponseMessage Get(string ext, string fileName) { string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext)); if (File.Exists(videoPath)) { FileInfo fi = new FileInfo(videoPath); var video = new VideoStream(videoPath); var response = Request.CreateResponse(); response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream, new MediaTypeHeaderValue("video/" + ext)); response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fi.Name.Replace(" ", "")); response.Content.Headers.Add("Content-Length", video.FileLength.ToString()); return response; } else { return Request.CreateResponse(HttpStatusCode.NotFound); } } }
شرح کلاس VideoController
همانطور که میبینید مسیر دستیابی به این کنترلر با استفاده از قابلیت Attribute Routing تعریف شده است.
[Route("api/video/{ext}/{fileName}")]
api/video/mp4/sample
متد Get این کنترلر دو پارامتر با نامهای ext و fileName را میپذیرد که همان فرمت و نام فایل هستند. سپس با استفاده از کلاس HostingEnvironment سعی میکنیم مسیر کامل فایل درخواست شده را بدست آوریم.
string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext));
var video = new VideoStream(videoPath);
var response = Request.CreateResponse(); response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream, new MediaTypeHeaderValue("video/" + ext));
کلاس PushStreamContent در فضای نام System.Net.Http وجود دارد. همانطور که میبینید امضای Action پاس داده شده، با امضای متد WriteToStream در کلاس VideoStream مطابقت دارد.
در آخر دو Header به پاسخ ارسالی اضافه میکنیم تا نوع داده ارسالی و سایز آن را مشخص کنیم.
response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fileName); response.Content.Headers.Add("Content-Length", video.FileLength.ToString());
if(File.Exists(videoPath)) { // removed for bravity } else { return Request.CreateResponse(HttpStatusCode.NotFound); }
public class HomeController : Controller { // GET: Home public ActionResult Index() { return View(); } }
<div> <div> <video width="480" height="270" controls="controls" preload="auto"> <source src="/api/video/mp4/sample" type="video/mp4" /> Your browser does not support the video tag. </video> </div> </div>
اگر پروژه را اجرا کنید میبینید که ویدیو مورد نظر آماده پخش است. برای اینکه ببینید چطور دادههای ویدیو در قالب پکیجهای 64 کیلو بایتی دریافت میشوند از ابزار مرورگرتان استفاده کنید. مثلا در گوگل کروم F12 را بزنید و به قسمت Network بروید. صفحه را یکبار مجددا بارگذاری کنید تا ارتباطات شبکه مانیتور شود. اگر به المنت sample دقت کنید میبینید که با شروع پخش ویدیو پکیجهای اطلاعات یکی پس از دیگری دریافت میشوند و اطلاعات ریز آن را میتوانید مشاهده کنید.
پروژه نمونه به این مقاله ضمیمه شده است. قابلیت Package Restore فعال شده و برای صرفه جویی در حجم فایل، تمام پکیجها و محتویات پوشه bin حذف شده اند. برای تست بیشتر میتوانید فایل sample.mp4 را با فایلی حجیمتر جایگزین کنید تا نحوه دریافت اطلاعات را با روشی که در بالا بدان اشاره شد مشاهده کنید.
AsyncVideoStreaming.rar
در این بین به راحتی میتوان چندین نمونه از این ORMها را نام برد مثل IBatis , Hibernate ,Nhibernate و EF که از معروفترین آنها هستند.
من در حال حاضر قصد شروع یک پروژه اندرویدی را دارم و دوست دارم بجای استفادهی از Sqlitehelper، از یک ORM مناسب بهره ببرم که چند سوال برای من پیش میآید. آیا ORM ای برای آن تهیه شده است؟ اگر آری چندتا و کدامیک از آنها بهتر هستند؟ شاید در اولین مورد کتابخانهی Hibernate جاوا را نام ببرید؛ ولی توجه به این نکته ضروری است که ما در مورد پلتفرم موبایل و محدودیتهای آن صحبت میکنیم. یک کتابخانه همانند Hibernate مطمئنا برای یک برنامه اندروید چه از نظر حجم نهایی برنامه و چه از نظر حجم بزرگش در اجرا، مشکل زا خواهد بود و وجود وابستگیهای متعدد و دارا بودن بسیاری از قابلیتهایی که اصلا در بانکهای اطلاعاتی موبایل قابل اجرا نیست، باعث میشود این فریمورک انتخاب خوبی برای یک برنامه اندروید نباشد.
معیارهای انتخاب یک فریم ورک مناسب برای موبایل:
- سبک بودن: مهمترین مورد سبک بودن آن است؛ چه از لحاظ اجرای برنامه و چه از لحاظ حجم نهایی برنامه
- سریع بودن: مطمئنا ORMهای طراحی شدهی موجود، از سرعت خیلی بدی برخوردار نخواهند بود؛ اگر سر زبان هم افتاده باشند. ولی باز هم انتخاب سریع بودن یک ORM، مورد علاقهی بسیاری از ماهاست.
- یادگیری آسان و کانفیگ راحت تر.
OrmLight
این فریمورک مختص اندروید طراحی نشده ولی سبک بودن آن موجب شدهاست که بسیاری از برنامه نویسان از آن در برنامههای اندرویدی استفاده کنند. این فریم ورک جهت اتصالات JDBC و Spring و اندروید طراحی شده است.
نحوه معرفی جداول در این فریمورک به صورت زیر است:
@DatabaseTable(tableName = "users") public class User { @DatabaseField(id = true) private String username; @DatabaseField private String password; public User() { // ORMLite needs a no-arg constructor } public User(String username, String password) { this.username = username; this.password = password; } // Implementing getter and setter methods public String getUserame() { return this.username; } public void setName(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } }
SugarORM
این فریمورک مختص اندروید طراحی شده است. یادگیری آن بسیار آسان است و به راحتی به یاد میماند. همچنین جداول مورد نیاز را به طور خودکار خواهد ساخت. روابط یک به یک و یک به چند را پشتیبانی میکند و عملیات CURD را با سه متد Save,Delete و Find که البته FindById هم جزء آن است، پیاده سازی میکند.
برای استفاده از این فریمورک نیاز است ابتدا متادیتاهای زیر را به فایل manifest اضافه کنید:
<meta-data android:name="DATABASE" android:value="my_database.db" /> <meta-data android:name="VERSION" android:value="1" /> <meta-data android:name="QUERY_LOG" android:value="true" /> <meta-data android:name="DOMAIN_PACKAGE_NAME" android:value="com.my-domain" />
public class User extends SugarRecord<User> { String username; String password; int age; @Ignore String bio; //this will be ignored by SugarORM public User() { } public User(String username, String password,int age){ this.username = username; this.password = password; this.age = age; } }
باقی عملیات آن از قبیل اضافه کردن یک رکورد جدید یا حذف رکورد(ها) به صورت زیر است:
User johndoe = new User(getContext(),"john.doe","secret",19); johndoe.save(); //ذخیره کاربر جدید در دیتابیس //حذف تمامی کاربرانی که سنشان 19 سال است List<User> nineteens = User.find(User.class,"age = ?",new int[]{19}); foreach(user in nineteens) { user.delete(); }
موقعیکه بحث کارآیی و سرعت پیش میآید نام GreenDAO هست که میدرخشد. طبق گفتهی سایت رسمی آن این فریمورک میتواند در ثانیه چند هزار موجودیت را اضافه و به روزرسانی و بارگیری نماید. این لیست حاوی برنامههایی است که از این فریمورک استفاده میکنند. جدول زیر مقایسهای است بین این کتابخانه و OrmLight که نشان میدهد 4.5 برابر سریعتر از OrmLight عمل میکند.
آموزش راه اندازی آن در اندروید استادیو، سورس آن در گیت هاب و مستندات رسمی آن.
Active Android
این کتابخانه از دو طریق فایل JAR و به شیوه maven قابل استفاده است که میتوانید روش استفادهی از آن را در این لینک ببینید و سورس اصلی آن هم در این آدرس قرار دارد. بعد از اینکه کتابخانه را به پروژه اضافه کردید، دو متادیتای زیر را که به ترتیب نام دیتابیس و ورژن آن هستند، به manifest اضافه کنید:
<meta-data android:name="AA_DB_NAME" android:value="my_database.db" /> <meta-data android:name="AA_DB_VERSION" android:value="1" />
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActiveAndroid.initialize(this); //ادامه برنامه } }
@Table(name = "User") public class User extends Model { @Column(name = "username") public String username; @Column(name = "password") public String password; public User() { super(); } public User(String username,String password) { super(); this.username = username; this.password = password; } }
ORMDroid
از آن دست کتابخانههایی است که سادگی و کم حجم بودن شعار آنان است و سعی دارند تا حد ممکن همه چیز را خودکار کرده و کمترین کانفیگ را نیاز داشته باشد. حجم فعلی آن حدود 20 کیلوبایت بوده و نمیخواهند از 30 کیلوبایت تجاوز کند.
برای استفادهی از آن ابتدا دو خط زیر را جهت معرفی تنظیمات به manifest اضافه کنید:
<meta-data android:name="ormdroid.database.name" android:value="your_database_name" /> <meta-data android:name="ormdroid.database.visibility" android:value="PRIVATE||WORLD_READABLE||WORLD_WRITEABLE" />
ORMDroidApplication.initialize(someContext);
public class Person extends Entity { public int id; public String name; public String telephone; } //==================== Person p = Entity.query(Person.class).where("id=1").execute(); p.telephone = "555-1234"; p.save(); // یا Person person = Entity.query(Person.class).where(eql("id", id)).execute(); p.telephone = "555-1234"; p.save();
سورس آن در گیت هاب
در اینجا سعی کردیم تعدادی از کتابخانههای محبوب را معرفی کنیم ولی تعداد آن به همین جا ختم نمیشود. ORMهای دیگری نظیر AndRom و سایر ORM هایی که در این لیست معرفی شده اند وجود دارند.
نکته نهایی اینکه خوب میشود دوستانی که از این ORMهای مختص اندروید استفاده کرده اند؛ نظراتشان را در مورد آنها بیان کنند و مزایا و معایب آنها را بیان کنند.