۱۳۹۱/۰۲/۱۵

EF Code First #2


در قسمت قبل با تنظيمات و قراردادهاي ابتدايي EF Code first آشنا شديم، هرچند اين تنظيمات حجم كدنويسي ابتدايي راه اندازي سيستم را به شدت كاهش مي‌دهند، اما كافي نيستند. در اين قسمت نگاهي سطحي و مقدماتي خواهيم داشت بر امكانات مهيا جهت تنظيم ويژگي‌هاي مدل‌هاي برنامه در EF Code first.

تنظيمات EF Code first توسط اعمال متاديتاي خواص

اغلب متاديتاي مورد نياز جهت اعمال تنظيمات EF Code first در اسمبلي System.ComponentModel.DataAnnotations.dll قرار دارند. بنابراين اگر مدل‌هاي خود را در اسمبلي و پروژه class library جداگانه‌اي تعريف و نگهداري مي‌كنيد (مثلا به نام DomainClasses)، نياز است ابتدا ارجاعي را به اين اسمبلي به پروژه جاري اضافه نمائيم. همچنين تعدادي ديگر از متاديتاي قابل استفاده در خود اسمبلي EntityFramework.dll قرار دارند. بنابراين در صورت نياز بايد ارجاعي را به اين اسمبلي نيز اضافه نمود.
همان مثال قبل را در اينجا ادامه مي‌دهيم. دو كلاس Blog و Post در آن تعريف شده (به اين نوع كلاس‌ها POCO – the Plain Old CLR Objects نيز گفته مي‌شود)، به همراه كلاس Context كه از كلاس DbContext مشتق شده است. ابتدا ديتابيس قبلي را دستي drop كنيد. سپس در كلاس Blog، خاصيت public int Id را مثلا به public int MyTableKey تغيير دهيد و پروژه را اجرا كنيد. برنامه بلافاصله با خطاي زير متوقف مي‌شود:

One or more validation errors were detected during model generation:
\tSystem.Data.Entity.Edm.EdmEntityType: : EntityType 'Blog' has no key defined.

زيرا EF Code first در اين كلاس خاصيتي به نام Id يا BlogId را نيافته‌است و امكان تشكيل Primary key جدول را ندارد. براي رفع اين مشكل تنها كافي است ويژگي Key را به اين خاصيت اعمال كنيم:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
    public class Blog
    {
        [Key]
        public int MyTableKey { set; get; }

همچنين تعدادي ويژگي ديگر مانند MaxLength و Required را نيز مي‌توان بر روي خواص كلاس اعمال كرد:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
    public class Blog
    {
        [Key]
        public int MyTableKey { set; get; }

        [MaxLength(100)]
        public string Title { set; get; }

        [Required]
        public string AuthorName { set; get; }

        public IList<Post> Posts { set; get; }        
    }
}

اين ويژگي‌ها دو مقصود مهم را برآورده مي‌سازند:
الف) بر روي ساختار بانك اطلاعاتي تشكيل شده تاثير دارند:

CREATE TABLE [dbo].[Blogs](
 [MyTableKey] [int] IDENTITY(1,1) NOT NULL,
 [Title] [nvarchar](100) NULL,
 [AuthorName] [nvarchar](max) NOT NULL,
 CONSTRAINT [PK_Blogs] PRIMARY KEY CLUSTERED 
(
 [MyTableKey] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

همانطور كه ملاحظه مي‌كنيد در اينجا طول فيلد Title به 100 تنظيم شده است و همچنين فيلد AuthorName اينبار NOT NULL است. به علاوه primary key نيز بر اساس ويژگي Key اعمالي تعيين شده است.
البته براي اجراي كدهاي تغيير كرده مدل، فعلا بانك اطلاعاتي قبلي را دستي مي‌توان حذف كرد تا بتوان به ساختار جديد رسيد. در مورد جزئيات مبحث DB Migration در قسمت‌هاي بعدي مفصلا بحث خواهد شد.

ب) اعتبار سنجي اطلاعات پيش از ارسال كوئري به بانك اطلاعاتي
براي مثال اگر در حين تعريف وهله‌اي از كلاس Blog، خاصيت AuthorName مقدار دهي نگردد، پيش از اينكه رفت و برگشتي به بانك اطلاعاتي صورت گيرد، يك validation error را دريافت خواهيم كرد. يا براي مثال اگر طول اطلاعات خاصيت Title بيش از 100 حرف باشد نيز مجددا در حين ثبت اطلاعات، يك استثناي اعتبار سنجي را مشاهده خواهيم كرد. البته امكان تعريف پيغام‌هاي خطاي سفارشي نيز وجود دارد. براي اين حالت تنها كافي است پارامتر ErrorMessage اين ويژگي‌ها را مقدار دهي كرد. براي مثال:
[Required(ErrorMessage = "لطفا نام نويسنده را مشخص نمائيد")]
public string AuthorName { set; get; }

نكته‌ي مهمي كه در اينجا وجود دارد، وجود يك اكوسيستم هماهنگ و سازگار است. اين نوع اعتبار سنجي هم با EF Code first هماهنگ است و هم براي مثال در ASP.NET MVC به صورت خودكار جهت اعتبار سنجي سمت سرور و كلاينت يك مدل مي‌تواند مورد استفاده قرار گيرد و مفاهيم و روش‌هاي مورد استفاده در آن نيز يكي است.


تنظيمات EF Code first به كمك Fluent API

اگر علاقمند به استفاده از متاديتا، جهت تعريف قيود و ويژگي‌هاي خواص كلاس‌هاي مدل خود نيستيد، روش ديگري نيز در EF Code first به نام Fluent API تدارك ديده شده است. در اينجا امكان تعريف همان ويژگي‌ها توسط كدنويسي نيز وجود دارد، به علاوه اعمال قيود ديگري كه توسط متاديتاي مهيا قابل تعريف نيستند.
محل تعريف اين قيود، كلاس Context كه از كلاس DbContext مشتق شده است، مي‌باشد و در اينجا، كار با تحريف متد OnModelCreating شروع مي‌شود:

using System.Data.Entity;
using EF_Sample01.Models;

namespace EF_Sample01
{
    public class Context : DbContext
    {
        public DbSet<Blog> Blogs { set; get; }
        public DbSet<Post> Posts { set; get; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>().HasKey(x => x.MyTableKey);
            modelBuilder.Entity<Blog>().Property(x => x.Title).HasMaxLength(100);
            modelBuilder.Entity<Blog>().Property(x => x.AuthorName).IsRequired();
            
            base.OnModelCreating(modelBuilder);
        }
    }
}

به كمك پارامتر modelBuilder، امكان دسترسي به متدهاي تنظيم كننده ويژگي‌هاي خواص يك مدل يا موجوديت وجود دارد. در اينجا چون مي‌توان متدها را به صورت يك زنجيره به هم متصل كرد و همچنين حاصل نهايي شبيه به جمله بندي انگليسي است، به آن Fluent API يا API روان نيز گفته مي‌شود.
البته در اين حالت امكان تعريف ErrorMessage وجود ندارد و براي اين منظور بايد از همان data annotations استفاده كرد.


نحوه مديريت صحيح تعاريف نگاشت‌ها به كمك Fluent API

OnModelCreating محل مناسبي جهت تعريف حجم انبوهي از تنظيمات كلاس‌هاي مختلف مدل‌هاي برنامه نيست. در حد سه چهار سطر مشكلي ندارد اما اگر بيشتر شد بهتر است از روش زير استفاده شود:

using System.Data.Entity;
using EF_Sample01.Models;
using System.Data.Entity.ModelConfiguration;

namespace EF_Sample01
{
    public class BlogConfig : EntityTypeConfiguration<Blog>
    {
        public BlogConfig()
        {
            this.Property(x => x.Id).HasColumnName("MyTableKey");
            this.Property(x => x.RowVersion).HasColumnType("Timestamp");
        }
    }


با ارث بري از كلاس EntityTypeConfiguration،‌ مي‌توان به ازاي هر كلاس مدل، تنظيمات را جداگانه انجام داد. به اين ترتيب اصل SRP يا Single responsibility principle نقض نخواهد شد. سپس براي استفاده از اين كلاس‌هاي Config تك مسئوليتي به نحو زير مي‌توان اقدام كرد:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
     modelBuilder.Configurations.Add(new BlogConfig());




نحوه تنظيمات ابتدايي نگاشت كلاس‌ها به بانك اطلاعاتي در EF Code first

الزامي ندارد كه EF Code first حتما با يك بانك اطلاعاتي از نو تهيه شده بر اساس پيش فرض‌هاي آن كار كند. در اينجا مي‌توان از بانك‌هاي اطلاعاتي موجود نيز استفاده كرد. اما در اين حالت نياز خواهد بود تا مثلا نام جدولي خاص با كلاسي مفروض در برنامه، يا نام فيلدي خاص كه مطابق استانداردهاي نامگذاري خواص در سي شارپ تعريف نشده، با خاصيتي در يك كلاس تطابق داده شوند. براي مثال اينبار تعاريف كلاس Blog را به نحو زير تغيير دهيد:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
    [Table("tblBlogs")]
    public class Blog
    {
        [Column("MyTableKey")]
        public int Id { set; get; }

        [MaxLength(100)]
        public string Title { set; get; }

        [Required(ErrorMessage = "لطفا نام نويسنده را مشخص نمائيد")]
        public string AuthorName { set; get; }

        public IList<Post> Posts { set; get; }

        [Timestamp]
        public byte[] RowVersion { set; get; }
    }
}

در اينجا فرض بر اين است كه نام جدول متناظر با كلاس Blog در بانك اطلاعاتي مثلا tblBlogs است و نام خاصيت Id در بانك اطلاعاتي مساوي فيلدي است به نام MyTableKey. چون نام خاصيت را مجددا به Id تغيير داده‌ايم، ديگر ضرورتي به ذكر ويژگي Key وجود نداشته است. براي تعريف اين دو از ويژگي‌هاي Table و Column جهت سفارشي سازي نام‌هاي خواص و كلاس استفاده شده است.
يا اگر در كلاس خود خاصيتي محاسبه شده بر اساس ساير خواص، تعريف شده است و قصد نداريم آن‌را به فيلدي در بانك اطلاعاتي نگاشت كنيم، مي‌توان از ويژگي NotMapped براي مزين سازي و تعريف آن كمك گرفت.
به علاوه اگر از نام پيش فرض كليد خارجي تشكيل شده خرسند نيستيد مي‌توان به كمك ويژگي ForeignKey، نسبت به تعريف مقداري جديد مطابق تعاريف يك بانك اطلاعاتي موجود، اقدام كرد.
همچنين خاصيت ديگري به نام RowVersion در اينجا اضافه شده كه با ويژگي TimeStamp مزين گرديده است. از اين خاصيت ويژه براي بررسي مسايل همزماني ثبت اطلاعات در EF استفاده مي‌شود. به علاوه بانك اطلاعاتي مي‌تواند به صورت خودكار آن‌را در حين ثبت مقدار دهي كند.
تمام اين تغييرات را به كمك Fluent API نيز مي‌توان انجام داد:

modelBuilder.Entity<Blog>().ToTable("tblBlogs");
modelBuilder.Entity<Blog>().Property(x => x.Id).HasColumnName("MyTableKey");
modelBuilder.Entity<Blog>().Property(x => x.RowVersion).HasColumnType("Timestamp");



تبديل پروژه‌هاي قديمي EF به كلاس‌هاي EF Code first به صورت خودكار

روش متداول كار با EF از روز اول آن، مهندسي معكوس خودكار اطلاعات يك بانك اطلاعاتي و تبديل آن به يك فايل EDMX بوده است. هنوز هم مي‌توان از اين روش در اينجا نيز بهره جست. براي مثال اگر قصد داريد يك پروژه قديمي را تبديل به نمونه جديد Code first كنيد، يا يك بانك اطلاعاتي موجود را مهندسي معكوس كنيد، بر روي پروژه در Solution explorer كليك راست كرده و گزينه Add|New Item را انتخاب كنيد. سپس از صفحه ظاهر شده، ADO.NET Entity data model را انتخاب كرده و در ادامه گزينه «Generate from database» را انتخاب كنيد. اين روال مرسوم كار با EF Database first است.
پس از اتمام كار به entity data model designer مراجعه كرده و بر روي صفحه كليك راست نمائيد. از منوي ظاهر شده گزينه «Add code generation item» را انتخاب كنيد. سپس در صفحه باز شده از ليست قالب‌هاي موجود، گزينه «ADO.NET DbContext Generator» را انتخاب نمائيد. اين گزينه به صورت خودكار اطلاعات فايل EDMX قديمي يا موجود شما را تبديل به كلاس‌هاي مدل Code first معادل به همراه كلاس DbContext معرف آن‌ها خواهد كرد.

روش ديگري نيز براي انجام اينكار وجود دارد. نياز است افزونه‌ي به نام Entity Framework Power Tools را دريافت كنيد. پس از نصب، از منوي Entity Framework آن گزينه‌ي «Reverse Engineer Code First» را انتخاب نمائيد. در اينجا مي‌توان مشخصات اتصال به بانك اطلاعاتي را تعريف و سپس نسبت به توليد خودكار كدهاي مدل‌ها و DbContext مرتبط اقدام كرد.



استراتژي‌هاي مقدماتي تشكيل بانك اطلاعاتي در EF Code first

اگر مثال اين سري را دنبال كرده باشيد، مشاهده كرده‌ايد كه با اولين بار اجراي برنامه، يك بانك اطلاعاتي پيش فرض نيز توليد خواهد شد. يا اگر تعاريف ويژگي‌هاي يك فيلد را تغيير داديم، نياز است تا بانك اطلاعاتي را دستي drop كرده و اجازه دهيم تا بانك اطلاعاتي جديدي بر اساس تعاريف جديد مدل‌ها تشكيل شود كه ... هيچكدام از اين‌ها بهينه نيستند.
در اينجا دو استراتژي مقدماتي را در حين آغاز يك برنامه مي‌توان تعريف كرد:

System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>());
// or
System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseAlways<Context>());

مي‌توان بانك اطلاعاتي را در صورت تغيير اطلاعات يك مدل به صورت خودكار drop كرده و نسبت به ايجاد نمونه‌اي جديد اقدام كرد (DropCreateDatabaseIfModelChanges)؛ يا در حين آزمايش برنامه هميشه (DropCreateDatabaseAlways) با شروع برنامه، ابتدا بايد بانك اطلاعاتي drop شده و سپس نمونه جديدي توليد گردد.
محل فراخواني اين دستور هم بايد در نقطه آغازين برنامه، پيش از وهله سازي اولين DbContext باشد. مثلا در برنامه‌هاي وب در متد Application_Start فايل global.asax.cs يا در برنامه‌هاي WPF در متد سازنده كلاس App مي‌توان بانك اطلاعاتي را آغاز نمود.
البته الزامي به استفاده از كلاس‌هاي DropCreateDatabaseIfModelChanges يا DropCreateDatabaseAlways وجود ندارد. مي‌توان با پياده سازي اينترفيس IDatabaseInitializer از نوع كلاس Context تعريف شده در برنامه، همان عمليات را شبيه سازي كرد يا سفارشي نمود:

public class MyInitializer : IDatabaseInitializer<Context>
{
        public void InitializeDatabase(Context context)
        {
            if (context.Database.Exists() ||
                context.Database.CompatibleWithModel(throwIfNoMetadata: false))
                context.Database.Delete();

            context.Database.Create();            
        }
}

سپس براي استفاده از اين كلاس در ابتداي برنامه، خواهيم داشت:

System.Data.Entity.Database.SetInitializer(new MyInitializer());


نكته:
اگر از يك بانك اطلاعاتي موجود استفاده مي‌كنيد (محيط كاري) و نيازي به پيش فرض‌هاي EF Code first نداريد و همچنين اين بانك اطلاعاتي نيز نبايد drop شود يا تغيير كند، مي‌توانيد تمام اين پيش فرض‌ها را با دستور زير غيرفعال كنيد:

Database.SetInitializer<Context>(null);

بديهي است اين دستور نيز بايد پيش از ايجاد اولين وهله از شيء DbContext فراخواني شود.


همچنين بايد درنظر داشت كه در آخرين نگارش‌هاي پايدار EF Code first، اين موارد بهبود يافته‌اند و مبحثي تحت عنوان DB Migration ايجاد شده است تا نيازي نباشد هربار بانك اطلاعاتي drop شود و تمام اطلاعات از دست برود. مي‌توان صرفا تغييرات كلاس‌ها را به بانك اطلاعاتي اعمال كرد كه به صورت جداگانه، در قسمتي مجزا بررسي خواهد شد. به اين ترتيب ديگر نيازي به drop بانك اطلاعاتي نخواهد بود. به صورت پيش فرض در صورت از دست رفتن اطلاعات يك استثناء را سبب خواهد شد (كه توسط برنامه نويس قابل تنظيم است) و در حالت خودكار يا دستي با تنظيمات ويژه قابل اعمال است.



تنظيم استراتژي‌هاي آغاز بانك اطلاعاتي در فايل كانفيگ برنامه

الزامي ندارد كه حتما متد Database.SetInitializer را دستي فراخواني كنيم. با اندكي تنظيم فايل‌هاي app.config و يا web.config نيز مي‌توان نوع استراتژي مورد استفاده را تعيين كرد:

<appSettings>
 <add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly" 
     value="MyNamespace.MyInitializerClass, MyAssembly"  />
</appSettings>

<appSettings>
 <add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly" 
     value="Disabled" />
</appSettings>

يكي از دو حالت فوق بايد در قسمت appSettings فايل كانفيگ برنامه تنظيم شود. حالت دوم براي غيرفعال كردن پروسه آغاز بانك اطلاعاتي و اعمال تغييرات به آن، بكار مي‌رود.
براي نمونه در مثال جاري، جهت استفاده از كلاس MyInitializer فوق، مي‌توان از تنظيم زير نيز استفاده كرد:

<appSettings>
    <add key="DatabaseInitializerForType EF_Sample01.Context, EF_Sample01"
       value="EF_Sample01.MyInitializer, EF_Sample01" />
  </appSettings>



اجراي كدهاي ويژه در حين تشكيل يك بانك اطلاعاتي جديد

امكان سفارشي سازي اين آغاز كننده‌هاي پيش فرض نيز وجود دارد. براي مثال:

public class MyCustomInitializer : DropCreateDatabaseIfModelChanges<Context>
{
    protected override void Seed(Context context)
    {
        context.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
        context.Database.ExecuteSqlCommand("CREATE INDEX IX_title ON tblBlogs (title)");
        base.Seed(context);
    }
}

در اينجا با ارث بري از كلاس DropCreateDatabaseIfModelChanges يك آغاز كننده سفارشي را تعريف كرده‌ايم. سپس با تحريف متد Seed آن مي‌توان در حين آغاز يك بانك اطلاعاتي، تعدادي ركورد پيش فرض را به آن افزود. كار ذخيره سازي نهايي در متد base.Seed انجام مي‌شود.
براي استفاده از آن اينبار در حين فراخواني متد System.Data.Entity.Database.SetInitializer، از كلاس MyCustomInitializer استفاده خواهيم كرد.
و يا توسط متد context.Database.ExecuteSqlCommand مي‌توان دستورات SQL را مستقيما در اينجا اجرا كرد. عموما دستوراتي در اينجا مدنظر هستند كه توسط ORMها پشتيباني نمي‌شوند. براي مثال تغيير collation يك ستون يا افزودن يك ايندكس و مواردي از اين دست.


سطح دسترسي مورد نياز جهت فراخواني متد Database.SetInitializer

استفاده از متدهاي آغاز كننده بانك اطلاعاتي نياز به سطح دسترسي بر روي بانك اطلاعاتي master را در SQL Server دارند (زيرا با انجام كوئري بر روي اين بانك اطلاعاتي مشخص مي‌شود، آيا بانك اطلاعاتي مورد نظر پيشتر تعريف شده است يا خير). البته اين مورد حين كار با SQL Server CE شايد اهميتي نداشته باشد. بنابراين اگر كاربري كه با آن به بانك اطلاعاتي متصل مي‌شويم سطح دسترسي پاييني دارد نياز است Persist Security Info=True را به رشته اتصالي اضافه كرد. البته اين مورد را پس از انجام تغييرات بر روي بانك اطلاعاتي جهت امنيت بيشتر حذف كنيد (يا به عبارتي در محيط كاري Persist Security Info=False بايد باشد).

Server=(local);Database=yourDatabase;User ID=yourDBUser;Password=yourDBPassword;Trusted_Connection=False;Persist Security Info=True


تعيين Schema و كاربر فراخوان دستورات SQL

در EF Code first به صورت پيش فرض همه چيز بر مبناي كاربري با دسترسي مديريتي يا dbo schema در اس كيوال سرور تنظيم شده است. اما اگر كاربر خاصي براي كار با ديتابيس تعريف گردد كه در هاست‌هاي اشتراكي بسيار مرسوم است، ديگر از دسترسي مديريتي dbo خبري نخواهد بود. اينبار نام جداول ما بجاي dbo.tableName مثلا someUser.tableName مي‌باشند و عدم دقت به اين نكته، اجراي برنامه را غيرممكن مي‌سازد.
براي تغيير و تعيين صريح كاربر متصل شده به بانك اطلاعاتي اگر از متاديتا استفاده مي‌كنيد، روش زير بايد بكارگرفته شود:

[Table("tblBlogs", Schema="someUser")]    
public class Blog

و يا در حالت بكارگيري Fluent API به نحو زير قابل تنظيم است:

modelBuilder.Entity<Blog>().ToTable("tblBlogs", schemaName:"someUser");