۱۳۹۱/۰۲/۱۹

EF Code First #6


ادامه بررسي Fluent API جهت تعريف نگاشت كلاس‌ها به بانك اطلاعاتي

در قسمت‌هاي قبل با استفاده از متاديتا و data annotations جهت بررسي نحوه نگاشت اطلاعات كلاس‌ها به جداول بانك اطلاعاتي آشنا شديم. اما اين موارد تنها قسمتي از توانايي‌هاي Fluent API مهيا در EF Code first را ارائه مي‌دهند. يكي از دلايل آن هم به محدود بودن توانايي‌هاي ذاتي Attributes بر مي‌گردد. براي مثال حين كار با Attributes امكان استفاده از متغيرها يا lambda expressions و امثال آن وجود ندارد. به علاوه شايد عده‌اي علاقمند نباشند تا كلاس‌هاي خود را با data annotations شلوغ كنند.

در قسمت دوم اين سري، مروري مقدماتي داشتيم بر Fluent API. در آنجا ذكر شد كه امكان تعريف نگاشت‌ها به كمك توانايي‌هاي Fluent API به دو روش زير ميسر است:
الف) مي‌توان از متد protected override void OnModelCreating در كلاس مشتق شده از DbContext كار را شروع كرد.
ب) و يا اگر بخواهيم كلاس Context برنامه را شلوغ نكنيم بهتر است به ازاي هر كلاس مدل برنامه، يك كلاس mapping مشتق شده از EntityTypeConfiguration را تعريف نمائيم. سپس مي‌توان اين كلاس‌ها را در متد OnModelCreating ياد شده، توسط متد modelBuilder.Configurations.Add جهت استفاده و اعمال، معرفي كرد.

كلاس‌هاي مدلي را كه در اين قسمت بررسي خواهيم كرد، همان كلاس‌هاي User و Project قسمت سوم هستند و هدف اين قسمت بيشتر تطابق Fluent API با اطلاعات ارائه شده در قسمت سوم است؛ براي مثال در اينجا چگونه بايد از خاصيتي صرفنظر كرد، مسايل همزماني را اعمال نمود و امثال آن.
بنابراين يك پروژه جديد كنسول را آغاز نمائيد. سپس با كمك NuGet ارجاعات لازم را به اسمبلي‌هاي EF اضافه نمائيد.
در پوشه Models اين پروژه، سه كلاس تكميل شده زير، از قسمت سوم وجود دارند:
using System;
using System.Collections.Generic;

namespace EF_Sample03.Models
{
    public class User
    {
        public int Id { set; get; }
        public DateTime AddDate { set; get; }
        public string Name { set; get; }
        public string LastName { set; get; }

        public string FullName
        {
            get { return Name + " " + LastName; }
        }

        public string Email { set; get; }
        public string Description { set; get; }
        public byte[] Photo { set; get; }
        public IList<Project> Projects { set; get; }
        public byte[] RowVersion { set; get; }
        public InterestComponent Interests { set; get; }

        public User()
        {
            Interests = new InterestComponent();
        }
    }
}

using System;

namespace EF_Sample03.Models
{
    public class Project
    {
        public int Id { set; get; }
        public DateTime AddDate { set; get; }               
        public string Title { set; get; }
        public string Description { set; get; }
        public virtual User User { set; get; }
        public byte[] RowVesrion { set; get; }
    }
}

namespace EF_Sample03.Models
{
    public class InterestComponent
    {
        public string Interest1 { get; set; }
        public string Interest2 { get; set; }
    }
}


سپس يك پوشه جديد به نام Mappings را به پروژه اضافه نمائيد. به ازاي هر كلاس فوق، يك كلاس جديد را جهت تعاريف اطلاعات نگاشت‌ها به كمك Fluent API اضافه خواهيم كرد:

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

namespace EF_Sample03.Mappings
{
    public class InterestComponentConfig : ComplexTypeConfiguration<InterestComponent>
    {
        public InterestComponentConfig()
        {            
            this.Property(x => x.Interest1).HasMaxLength(450);
            this.Property(x => x.Interest2).HasMaxLength(450);
        }
    }
}

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

namespace EF_Sample03.Mappings
{
    public class ProjectConfig : EntityTypeConfiguration<Project>
    {
        public ProjectConfig()
        {
            this.Property(x => x.Description).IsMaxLength();
            this.Property(x => x.RowVesrion).IsRowVersion();            
        }
    }
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample03.Mappings
{
    public class UserConfig : EntityTypeConfiguration<User>
    {
        public UserConfig()
        {
            this.HasKey(x => x.Id);
            this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
            this.ToTable("tblUser", schemaName: "guest");
            this.Property(p => p.AddDate).HasColumnName("CreateDate").HasColumnType("date").IsRequired();
            this.Property(x => x.Name).HasMaxLength(450);
            this.Property(x => x.LastName).IsMaxLength().IsConcurrencyToken();
            this.Property(x => x.Email).IsFixedLength().HasMaxLength(255); //nchar(128)
            this.Property(x => x.Photo).IsOptional();
            this.Property(x => x.RowVersion).IsRowVersion();
            this.Ignore(x => x.FullName);
        }
    }
}

توضيحاتي در مورد كلاس‌هاي تنظيمات نگاشت‌هاي خواص به جداول و فيلدهاي بانك اطلاعاتي

نظم بخشيدن به تعاريف نگاشت‌ها
همانطور كه ملاحظه مي‌كنيد، جهت نظم بيشتر پروژه و شلوغ نشدن متد OnModelCreating كلاس Context برنامه، كه در ادامه كدهاي آن معرفي خواهد شد، به ازاي هر كلاس مدل، يك كلاس تنظيمات نگاشت‌ها را اضافه كرده‌ايم.
كلاس‌هاي معمولي نگاشت‌ها ازكلاس EntityTypeConfiguration مشتق خواهند شد و جهت تعريف كلاس InterestComponent به عنوان Complex Type، اينبار از كلاس ComplexTypeConfiguration ارث بري شده است.

تعيين طول فيلدها
در كلاس InterestComponentConfig، به كمك متد HasMaxLength، همان كار ويژگي MaxLength را مي‌توان شبيه سازي كرد كه در نهايت، طول فيلد nvarchar تشكيل شده در بانك اطلاعاتي را مشخص مي‌كند. اگر نياز است اين فيلد nvarchar از نوع max باشد، نيازي به تنظيم خاصي نداشته و حالت پيش فرض است يا اينكه مي‌توان صريحا از متد IsMaxLength نيز براي معرفي nvarchar max استفاده كرد.

تعيين مسايل همزماني
در قسمت سوم با ويژگي‌هاي ConcurrencyCheck و Timestamp آشنا شديم. در اينجا اگر نوع خاصيت byte array بود و نياز به تعريف آن به صورت timestamp وجود داشت، مي‌توان از متد IsRowVersion استفاده كرد. معادل ويژگي ConcurrencyCheck در اينجا، متد IsConcurrencyToken است.

تعيين كليد اصلي جدول
اگر پيش فرض‌هاي EF Code first مانند وجود خاصيتي به نام Id يا ClassName+Id رعايت شود، نيازي به كار خاصي نخواهد بود. اما اگر اين قراردادها رعايت نشوند،‌ مي‌توان از متد HasKey (كه نمونه‌اي از آن‌را در كلاس UserConfig فوق مشاهده مي‌كنيد)، استفاده كرد.

تعيين فيلدهاي توليد شده توسط بانك اطلاعاتي
به كمك متد HasDatabaseGeneratedOption،‌ مي‌توان مشخص كرد كه آيا يك فيلد Identity است و يا يك فيلد محاسباتي ويژه و يا هيچكدام.

تعيين نام جدول و schema آن
اگر نياز است از قراردادهاي نامگذاري خاصي پيروي شود، ‌مي‌توان از متد ToTable جهت تعريف نام جدول متناظر با كلاس جاري استفاده كرد. همچنين در اينجا امكان تعريف schema نيز وجود دارد.

تعيين نام و نوع سفارشي فيلدها
همچنين اگر نام فيلدها نيز بايد از قراردادهاي ديگري پيروي كنند، مي‌توان آن‌ها را به صورت صريح توسط متد HasColumnName معرفي كرد. اگر نياز است اين خاصيت به نوع خاصي در بانك اطلاعاتي نگاشت شود، بايد از متد HasColumnType كمك گرفت. براي مثال در اينجا بجاي نوع datetime، از نوع ويژه date استفاده شده است.

معرفي فيلدها به صورت nchar بجاي nvarchar
براي نمونه اگر قرار است هش كلمه عبور در بانك اطلاعاتي ذخيره شود، چون طول آن ثابت مي‌باشد، توصيه شده‌است كه بجاي nvarchar از nchar براي تعريف آن استفاده شود. براي اين منظور تنها كافي است از متد IsFixedLength استفاده شود. در اين حالت طول پيش فرض 128 براي فيلد درنظر گرفته خواهد شد. بنابراين اگر نياز است از طول ديگري استفاده شود، مي‌توان همانند سابق از متد HasMaxLength كمك گرفت.
ضمنا اين فيلدها همگي يونيكد هستند و با n شروع شده‌اند. اگر مي‌خواهيد از varchar يا char استفاده كنيد، مي‌توان از متد IsUnicode با پارامتر false استفاده كرد.

معرفي يك فيلد به صورت null پذير در سمت بانك اطلاعاتي
استفاده از متد IsOptional، فيلد را در سمت بانك اطلاعاتي به صورت فيلدي با امكان پذيرش مقادير null معرفي مي‌كند.
البته در اينجا به صورت پيش فرض byte arrayها به همين نحو معرفي مي‌شوند و تنظيم فوق صرفا جهت ارائه توضيحات بيشتر در نظر گرفته شد.

صرفنظر كردن از خواص محاسباتي در تعاريف نگاشت‌ها
با توجه به اينكه خاصيت FullName به صورت يك خاصيت محاسباتي فقط خواندني، در كدهاي برنامه تعريف شده است، با استفاده از متد Ignore، از نگاشت آن به بانك اطلاعاتي جلوگيري خواهيم كرد.


معرفي كلاس‌هاي تعاريف نگاشت‌ها به برنامه

استفاده از كلاس‌هاي Config فوق خودكار نيست و نياز است توسط متد modelBuilder.Configurations.Add معرفي شوند:

using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample03.Mappings;
using EF_Sample03.Models;

namespace EF_Sample03.DataLayer
{
    public class Sample03Context : DbContext
    {
        public DbSet<User> Users { set; get; }
        public DbSet<Project> Projects { set; get; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new InterestComponentConfig());
            modelBuilder.Configurations.Add(new ProjectConfig());
            modelBuilder.Configurations.Add(new UserConfig());

            //modelBuilder.ComplexType<InterestComponent>();
            //modelBuilder.Ignore<InterestComponent>(); 

            base.OnModelCreating(modelBuilder);
        }
    }

    public class Configuration : DbMigrationsConfiguration<Sample03Context>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(Sample03Context context)
        {
            base.Seed(context);
        }
    }
}

در اينجا كلاس Context برنامه مثال جاري را ملاحظه مي‌كنيد؛ به همراه كلاس Configuration مهاجرت خودكار كه در قسمت‌هاي قبل بررسي شد.
در متد OnModelCreating نيز مي‌توان يك كلاس را از نوع Complex معرفي كرد تا براي آن در بانك اطلاعاتي جدول جداگانه‌اي تعريف نشود. اما بايد دقت داشت كه اينكار را فقط يكبار مي‌توان انجام داد؛ يا توسط كلاس InterestComponentConfig و يا توسط متد modelBuilder.ComplexType. اگر هر دو با هم فراخواني شوند، EF يك استثناء را صادر خواهد كرد.

و در نهايت، قسمت آغازين برنامه اينبار به شكل زير خواهد بود كه از آغاز كننده MigrateDatabaseToLatestVersion (قسمت چهارم اين سري) نيز استفاده كرده است:

using System;
using System.Data.Entity;
using EF_Sample03.DataLayer;

namespace EF_Sample03
{
    class Program
    {
        static void Main(string[] args)
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample03Context, Configuration>());

            using (var db = new Sample03Context())
            {
                var project1 = db.Projects.Find(1);
                if (project1 != null)
                {
                    Console.WriteLine(project1.Title);
                }
            } 
        }
    }
}

ضمنا رشته اتصالي مورد استفاده تعريف شده در فايل كانفيگ برنامه نيز به صورت زير تعريف شده است:

<connectionStrings>
    <clear/>
    <add
       name="Sample03Context"
       connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
       providerName="System.Data.SqlClient"
      />
/connectionStrings>


در قسمت‌هاي بعد مباحث پيشرفته‌تري از تنظيمات نگاشت‌ها را به كمك Fluent API، بررسي خواهيم كرد. براي مثال روابط ارث بري، many-to-many و ... چگونه تعريف مي‌شوند.