Entity Framework 5.0系列之約定配置

KenshinCui發表於2013-09-08

Code First之所以能夠讓開發人員以一種更加高效、靈活的方式進行資料操作有一個重要的原因在於它的約定配置。現在軟體開發越來複雜,大家也都試圖將軟體設計的越來越靈活,很多內容我們都希望是可配置的,但是過多的配置也會帶來很大的工作量,解決這個問題的方法就是約定。對於一些簡單的,不太可能經常變化的內容我們以一種約定的方式進行設計。使用過其他ORM框架的朋友可能知道一般ORM都有對應的對映配置檔案(一般是一個Xml檔案),但是EF並沒有。在EF中是以一種約定的方式進行表、列同實體類進行對映的,與此同時為了提高最大的靈活性EF中可以通過Fluent API和Data Annotations兩種方式對對映進行靈活配置。

EF預設約定

我們先來看一下EF對於資料類(概念模型,或域模型)的預設約定:

  • 將資料類的類名複數形式作為資料表名稱,並且使用“dbo”作為預設架構。

例如定義一個Person資料類,那麼將會自動生成“dbo.People”表。

  • 將資料類中的“ID”屬性或者“<類名>+ID”作為主鍵(不區分大小寫),並且如果該列為數值型別或者GUID列將作為標識列。

例如在Order類中如果有ID或者OrderID屬性將預設作為主鍵,二者均出現優先使用 “ID”屬性。

  • 使用導航屬性約束兩個表之間的關係,在從表資料類中除了導航屬性,推薦定義一個外來鍵屬性在從表資料類中(如果不指定將預設生成一個“<主表類名>+<主表類的主鍵名>”的外來鍵列;此外在主表中推薦定義一個集合從表屬性使用者導航,當然這個屬性不定義也可以正常生成外來鍵關係但是不利於使用),具體規則:“<導航屬性名>+<主表的主鍵屬性名>”或者“<主表類名>+<主鍵屬性名>”又或者“<主表的主鍵屬性名>”,其屬性名不區分大小寫並且如果出現多種匹配按照先後順序匹配;如果不存在外來鍵屬性則外來鍵關係註冊為可選的,否則註冊為必選項並且此時將設定級聯刪除關係;如果在從表類中有多個導航屬性對應同一個資料類那麼需要使用fluent API或者Data Annotations進行手動配置。

例如有一個Order類,主鍵為OrderID,在OrderDetail類中有一個導航屬性Order(Order型別),那麼當你定義一個OrderID在OrderDetail中,那麼在Order和OrderDetail直接將建立一個級聯刪除關係。

  • 當EF按照上述規則在資料類中沒有找到主鍵屬性時(或者通過fluent API、Data Annotations沒有定義)將認為此類為“複雜型別”(對於不瞭解複雜型別的朋友請點選這裡What is a Complex Type)。

例如在“Person”資料類中有一個“Name”屬性,但是資料庫中可能將“Name”分為FirstName和LastName儲存,此時就可以定義一個Name類,在此類中不定義主鍵列定義“FirstName”和“LastName”屬性,就會在表“dbo.People”中生成“Name_FirstName”和“Name_LastName”列。

定義約定

EF的預設約定不是一成不變的,我們可以選擇移除和修改它,例如EF預設生成資料表時將資料類名的複數形式作為表名,下面的程式碼就可以移除這個規則:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using CodeFirst.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace CodeFirst
{
    public class OrderContext:DbContext
    {
        public OrderContext()
            : base("CodeFirstDb")
        {
            Database.SetInitializer<OrderContext>(
                new DropCreateDatabaseIfModelChanges<OrderContext>()
            );
        }

        public DbSet<Person> Person
        {
            get;
            set;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        }
    }
}

這些規則都在“System.Data.Entity.ModelConfiguration.Conventions”名稱空間下,可以根據實際情況進行選擇。

一般情況下我們是不需要移除預設約定的,我們更多的時候是要修改豐富這些約定,達到對生成規則的更多細節控制。在EF提供了兩種方式進行對映配置:Data Annotations和Fluent API。

DataAnnotations

DataAnnotations是ASP.NET WEB中新增的一種驗證方式,但是在EF中它又可以對對映關係進行控制,相比較Fluent API使用起來要簡單一些。下面我們通過一個例子對DataAnnotations進行說明。在此我們假設有一個“Employee”類用於描述員工資訊,一個“Customer”類用於描述客戶資訊,還有一個“Order”類用於描述訂單資訊,此外還有一個“Name”複雜型別表示人員姓名。在Order類中有一個屬性“Customer”用於描述此訂單的客戶,它是“Customer”型別;還有一個“DeliverPerson”屬性用於描述訂單發貨人,一個“CheckPerson”屬性使用者描述訂單揀貨人,它們都是“Employee”型別。下面是具體程式碼:

Employee類:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    [Table("People",Schema="Person")]
    public class Employee
    {
        [Key]
        public int No
        {
            get;
            set;
        }

        public Name Name
        {
            get;
            set;
        }

        [MinLength(5),MaxLength(30)]
        public string Title
        {
            get;
            set;
        }

        [Required]
        public DateTime BirthDate
        {
            get;
            set;
        }

        [ConcurrencyCheck]
        public string Address
        {
            get;
            set;
        }

        [Column("Notes",TypeName="ntext",Order=5)]
        public string Note
        {
            get;
            set;
        }

        [DatabaseGenerated(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Computed)]
        public DateTime CreateDate
        {
            get;
            set;
        }

        [NotMapped]
        public string PhotoPath
        {
            get;
            set;
        }

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

        [InverseProperty("DeliverPerson")]
        public List<Order> DeliverOrder
        {
            get;
            set;
        }

        [InverseProperty("CheckPerson")]
        public List<Order> CheckOrder
        {
            get;
            set;
        }
    }
}

Customer類:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    public class Customer
    {
        public int CustomerID
        {
            get;
            set;
        }

        public string CompanyName
        {
            get;
            set;
        }
    }
}

Name類:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    [ComplexType]//根據前面我們說的預設約定,不標記為ComplexType只有沒有找到ID也會將Name作為一個複雜型別
    public class Name
    {
        public string FirstName
        {
            get;
            set;
        }

        public string LastName
        {
            get;
            set;
        }
    }
}

Order類:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    public class Order
    {
        public int OrderID 
        { 
            get; 
            set; 
        }

        public string OrderTitle 
        { 
            get; 
            set; 
        }

        public string CustomerName 
        { 
            get; 
            set; 
        }

        public DateTime TransactionDate 
        { 
            get; 
            set; 
        }

        public int CustomerNo
        {
            get;
            set;
        }

        [ForeignKey("CustomerNo")]
        public Customer Customer
        {
            get;
            set;
        }

        public Employee DeliverPerson
        {
            get;
            set;
        }

        public Employee CheckPerson
        {
            get;
            set;
        }
    }
}

這是通過Data Annotations配置後EF生成的資料庫表結構:

下面解釋每個配置的作用

Table:用於指定生成表的表名、架構資訊。

Column:用於指定生成資料表的列資訊,如列名、資料型別、順序等。

Key:用於指定任何名稱的屬性作為主鍵列並且預設將此列作為標識列(如果不想預設生成標識可以指定“DatabaseGenerated”屬性的值為“None”),如果不指定此標記屬性,將根據EF預設約定建立主鍵。如上程式碼指定“No”為“Employee”的主鍵。

Required:使用者指定非空列,如上面的“BirthDay”建立列之後為“not null”列。

MinLengthMaxLength:指定欄位長度(此屬性通常可以使用者客戶端驗證),例如上面“Title”定義成了“nvarchar(30)”。

ComplexType:用於標記複雜型別,對於包含複雜型別資料屬性的類在生成資料表時複雜型別中每個屬性都將作為其中一列。

DatabaseGenerated:用於指定資料庫欄位生成列,此類EF將不會直接更新。可以指定為計算列、標識列和非資料庫生成列(例如給主鍵列指定此屬性為“None”則不會生成標識列)。需要注意的是如果使用Code First欄位生成資料庫,那麼此屬性僅僅可以用於byte、timestamp列上,否則請應用在已經存在資料庫的情況下,因為Code First無法判定生成具體計算列的公式(至少目前Code First還不支援公式配置)。

NotMapped:使用者指定非對映列,標記此屬性的列將不會在資料庫中生成相應的列,例如上面的“PhotoPath ”沒有在資料庫中生成具體列,實際使用中它可能是通過其他具體規則得到的。

ConcurrencyCheck:用於進行併發檢查,當一個使用者A獲得實體後通常會與資料庫斷開,此時如果另一個使用者B也獲得了實體並進行了修改,那麼當A再進行更新時如果進行了“ConcurrencyCheck”標記則會進行併發檢查,並根據原始值判斷該實體是否存在,如果不存在則丟擲異常。

TimeStamp:用於指定時間戳列,一個實體只能有一個TimeStamp列。在EF中TimeStamp是另一種併發控制方式,當EF遇到TimeStamp列會自動配置 “ConcurrencyCheck”及“DatabaseGenerated.Computed”來控制併發(通常我們推薦使用此方法)。

ForeignKey:用於指定外來鍵列,我們知道按照上面提到的預設約定第三條,當我們在“Order”中定義了“Customer”屬性後,如果定義“CustomerID” 屬性(當然還有其他形式,大家可以按照宣告說的預設約定3進行測試),那麼EF會在“Order”表中建立一個“CustomerID”列並建立與“Customer”表的外來鍵關係。但是如果像上面定義“CustomerNo”屬性並且不指定“ForeignKey”標記的話將達不到我們的預期,EF將預設建立一個“Customer_CustomerID”列並建立與“Customer”的外來鍵約束,同時建立一個“CustomerNo”列。當然解決的方式大家已經看到了那就是給導航屬性“Customer”指定“ForegnKey”標記並且指定外來鍵列為“CustomerNo”(注意雖然在“Customer”中不定義“Order的導航屬性”生成的表中也並沒用任何問題,但是我們推薦您定義相應的導航屬性)。

InverseProperty:用於定義多重外來鍵關係約束。我們在EF中通過導航屬性定義主外來鍵關係,但是當主表中有兩個外來鍵約束時可能僅僅通過新增相應的導航屬性就無法完成了,例如上面“Order”中“DeliverPerson”和“CheckPerson”均為“Employee”型別,按我們的預期當然是在生成“Order”表時生成兩個外來鍵列並建立與“Employee”外來鍵約束關係,但是如果沒有在“Employee”類的“DeliverOrder”和“CheckOrder”屬性上標記 “InverseProperty”屬性EF是無法識別這種關係的(具體結果可以看下圖),當然解決方式就是在對應導航屬性中標記“InverseProperty”並指定對於的列名。

clip_image004

注意:DataAnnotations可以同時在同一個類後者屬性上使用多個標記屬性,上面的例子中對於每個類或屬性只使用了一個單獨的標記屬性是為了說明起來更加簡單;另外宣告的例子中同時使用“ConcurrencyCheck”和“TimeStamp”指定了不同的列只是為了演示,一般情況下我們通過其中一種方式即可。

Fluent API

Fluent API一般配置

儘管我們可以通過Data Annotations進行對映關係約定,但是相比較而言Fluent API的功能更加強大,從功能上而言Data Annotations是Fluent API的一個子集, Data Annotations可以實現的功能Fluent API都能實現。下面我們先看一個例子,在這個例子中我們通過Fluent API約定方式實現上面Data Annotations的功能並且包含更多控制:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using CodeFirst.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace CodeFirst
{
    public class OrderContext:DbContext
    {
        public OrderContext()
            : base("CodeFirstDb")
        {
            Database.SetInitializer<OrderContext>(
                new DropCreateDatabaseIfModelChanges<OrderContext>()
            );
        }

        public DbSet<Order> Orders
        {
            get;
            set;
        }

        public DbSet<Employee> Employees
        {
            get;
            set;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<Employee>().ToTable("People", "Person");//指定“Employee”對應表名及架構

            modelBuilder.Entity<Employee>().HasKey(emp => emp.No);//定義主鍵為“No”
            //modelBuilder.Entity<Employee>().HasKey(emp => new { emp.No, emp.Title });//指定"No"和“Title”作為複合主鍵,使用Data Annotations無法做到

            modelBuilder.Entity<Employee>().Property(emp => emp.No).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);//去掉標識列,前面我們也提到過通過Data Annotations通用可以去掉主鍵預設標示屬性

            modelBuilder.Entity<Employee>().Property(emp => emp.Title).HasMaxLength(30);//指定“Title”最大長度為30

            modelBuilder.Entity<Employee>().Property(emp => emp.BirthDate).IsRequired();//指定“BirthDate”為不可為空

            modelBuilder.Entity<Employee>().Property(emp => emp.Note).HasColumnName("Notes").HasColumnType("ntext");//指定“Note”對應列名為“Notes”,並指定資料型別

            modelBuilder.Entity<Employee>().Ignore(emp => emp.PhotoPath);//指定“PhotoPath”為非對映列

            modelBuilder.Entity<Employee>().Property(emp => emp.Title).IsUnicode(true);//“Title”列是否支援Unicode編碼

            modelBuilder.ComplexType<Name>().Property(n=>n.FirstName).HasMaxLength(50);//指定Name為複雜資料型別,並指定複雜型別中“FirstName”長度
            //modelBuilder.Entity<Employee>().Property(emp => emp.Name.LastName).HasMaxLength(20);//還可以通過這種方式指定複雜型別“Name”的“LastName”列的長度

            //modelBuilder.Entity<Employee>().Property(emp => emp.Address).IsConcurrencyToken();//指定“Address”進行併發控制,通常這一列我們知道為“TimeStamp”列而不是“Addree”這裡只是為了說明可以標記其他列
            modelBuilder.Entity<Employee>().Property(emp => emp.TimeStamp).IsRowVersion();//通過指定“TimeStamp”進行併發版本控制
        }
    }
}

從上面的程式碼中可以看到基本上在Data Annotations中實現的功能使用Fluent API都實現了,並且在上面的程式碼註釋中我也提到了一些Data Annotations無法實現的功能,具體程式碼基本上都已經註釋了在此也不再解釋了。

Fluent API關係配置

下面讓看一下EF中關係配置的實現,看一下Fluent API如何進行實體關係約束,這裡假設每個公司員工“Employee”在企業內部都有一個通訊賬戶“MessagingAcount”,這二者之間是一對一的關係;同時新增一個產品類“Product”,它與“Order”的關係是多對多的關係,具體定義如下:

MessageAcount類:

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

namespace CodeFirst.Entities
{
    public class MessagingAccount
    {
        [Key()]
        public int EmployeeNo
        {
            get;
            set;
        }

        public Employee Employee
        {
            get;
            set;
        }

        public string UserName
        {
            get;
            set;
        }

        public string Password
        {
            get;
            set;
        }
        
    }
}

Product類:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    public class Product
    {
        public int ProductID
        {
            get;
            set;
        }

        public string ProductName
        {
            get;
            set;
        }

        public double UnitPrice
        {
            get;
            set;
        }

        public int OrderID
        {
            get;
            set;
        }

        public List<Order> Orders
        {
            get;
            set;
        }

    }
}

Employee類(新增了對應的屬性):

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    public class Employee
    {
        public int No
        {
            get;
            set;
        }

        public Name Name
        {
            get;
            set;
        }

        public string Title
        {
            get;
            set;
        }

        public DateTime BirthDate
        {
            get;
            set;
        }

        public string Address
        {
            get;
            set;
        }

        public string Note
        {
            get;
            set;
        }

        public DateTime CreateDate
        {
            get;
            set;
        }

        public string PhotoPath
        {
            get;
            set;
        }

        public byte[] TimeStamp
        {
            get;
            set;
        }

        public List<Order> DeliverOrder
        {
            get;
            set;
        }

        public List<Order> CheckOrder
        {
            get;
            set;
        }

        public MessagingAccount Acount
        {
            get;
            set;
        }
    }
}

Order類(新增了對應的屬性):

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    public class Order
    {
        public int OrderID 
        { 
            get; 
            set; 
        }

        public string OrderTitle 
        { 
            get; 
            set; 
        }

        public string CustomerName 
        { 
            get; 
            set; 
        }

        public DateTime TransactionDate 
        { 
            get; 
            set; 
        }

        public int CustomerNo
        {
            get;
            set;
        }

        public Customer Customer
        {
            get;
            set;
        }

        public int ProductID
        {
            get;
            set;
        }

        public List<Product> Products
        {
            get;
            set;
        }
             

        public Employee DeliverPerson
        {
            get;
            set;
        }

        public Employee CheckPerson
        {
            get;
            set;
        }
    }
}

OrderContext類,定義了資料類之間的關係,主要是關係配置部分:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using CodeFirst.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace CodeFirst
{
    public class OrderContext:DbContext
    {
        public OrderContext()
            : base("CodeFirstDb")
        {
            Database.SetInitializer<OrderContext>(
                new DropCreateDatabaseIfModelChanges<OrderContext>()
            );
        }

        public DbSet<Order> Orders
        {
            get;
            set;
        }

        public DbSet<Employee> Employees
        {
            get;
            set;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<Employee>().ToTable("People", "Person");//指定“Employee”對應表名及架構

            modelBuilder.Entity<Employee>().HasKey(emp => emp.No);//定義主鍵為“No”
            //modelBuilder.Entity<Employee>().HasKey(emp => new { emp.No, emp.Title });//指定"No"和“Title”作為複合主鍵,使用Data Annotations無法做到

            modelBuilder.Entity<Employee>().Property(emp => emp.No).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);//去掉標識列,前面我們也提到過通過Data Annotations通用可以去掉主鍵預設標示屬性

            modelBuilder.Entity<Employee>().Property(emp => emp.Title).HasMaxLength(30);//指定“Title”最大長度為30

            modelBuilder.Entity<Employee>().Property(emp => emp.BirthDate).IsRequired();//指定“BirthDate”為不可為空

            modelBuilder.Entity<Employee>().Property(emp => emp.Note).HasColumnName("Notes").HasColumnType("ntext");//指定“Note”對應列名為“Notes”,並指定資料型別

            modelBuilder.Entity<Employee>().Ignore(emp => emp.PhotoPath);//指定“PhotoPath”為非對映列

            //modelBuilder.Entity<Customer>().HasRequired(c=>c.Orders).WithMany().Map(m=>m.MapKey("CustomerOrder"));

            modelBuilder.Entity<Employee>().Property(emp => emp.Title).IsUnicode(true);//“Title”列是否支援Unicode編碼

            modelBuilder.ComplexType<Name>().Property(n=>n.FirstName).HasMaxLength(50);//指定Name為複雜資料型別,並指定複雜型別中“FirstName”長度
            //modelBuilder.Entity<Employee>().Property(emp => emp.Name.LastName).HasMaxLength(20);//還可以通過這種方式指定複雜型別“Name”的“LastName”列的長度

            //modelBuilder.Entity<Employee>().Property(emp => emp.Address).IsConcurrencyToken();//指定“Address”進行併發控制,通常這一列我們知道為“TimeStamp”列而不是“Addree”這裡只是為了說明可以標記其他列
            modelBuilder.Entity<Employee>().Property(emp => emp.TimeStamp).IsRowVersion();//通過指定“TimeStamp”進行併發版本控制

            /*下面程式碼演示EF中的關係約束*/
            //modelBuilder.Entity<MessagingAccount>().HasRequired(a => a.Employee).WithOptional(emp => emp.Acount);//配置一對零關係,允許存在一個Employee而不存在MessagingAcount的情況(注意在Employee中新增Acont屬性)
            modelBuilder.Entity<Employee>().HasRequired(emp => emp.Acount).WithRequiredPrincipal(a => a.Employee);//配置一對一關係,和上面的WithOptionnal關係區別是每個Employee必須有一個MessagingAcount而每個MessageAcount也必須有一個Employee;但是Employee是主表,此時允許Employee單獨持久化而不允許MessagingAcount單獨持久化
            //注意配置一對一關係也可以使用WithRequiredDependent,只不過主表發生了變化,上面的語句與下面的語句是等價的
            //modelBuilder.Entity<MessagingAccount>().HasRequired(a => a.Employee).WithRequiredDependent(a => a.Acount);//
            //下面的方法解決了一對一的關係,此時Employee和MessageAcount將必須同時存在
            //modelBuilder.Entity<Employee>().HasRequired(emp => emp.Acount).WithMany().HasForeignKey(emp => emp.MessagingAccountID);
            //modelBuilder.Entity<MessagingAccount>().HasRequired(a => a.Employee).WithMany().HasForeignKey(a => a.EmployeeNo);

            //modelBuilder.Entity<Order>().HasRequired(o=>o.Customer).WithMany();//一對多的關係,一個Customer有多個Order(注意,執行之前先把Order中CustomerNo和Customer中的Orders屬性刪除掉,否則將生成兩個外來鍵一個是自動生成的另一個是Fluent API配置生成的,對應這種關係推薦使用預設生成)
            //modelBuilder.Entity<Order>().HasRequired(o => o.Customer).WithMany().WillCascadeOnDelete();//新增新增級聯刪除
            //modelBuilder.Entity<Order>().HasRequired(o => o.Customer).WithMany().Map(m=>m.MapKey("Customer_Order");//外來鍵重新命名
            //modelBuilder.Entity<Order>().HasRequired(o => o.Customer).WithMany().HasForeignKey(o => new { o.CustomerNo,o.CustomerName});//組合外來鍵,注意本例中沒有組合外來鍵(CustomerName不是外來鍵),這裡只是舉例而已
            //modelBuilder.Entity<Order>().HasRequired(o => o.Customer).WithMany(c=>c.Orders).HasForeignKey(o => o.CustomerNo);//指定外來鍵(一般使用者外來鍵名不符合預設約束命名時)

            modelBuilder.Entity<Order>().HasMany(o => o.Products).WithMany(p => p.Orders).Map(m => {
                m.ToTable("OrderDetails");
                m.MapLeftKey("OrderID");
                m.MapRightKey("ProductID");
            });//配置多對多的關係,並指定了表名、對應的外來鍵;注意如果不使用FluentAPI配置,Product和Order配置了相應的導航屬性,EF也會預設生成一張表(表名為“<資料類1>+<資料類2>”)
        }
    }
}

執行生成的資料庫結構如下圖:

在上面的程式碼中我們著重看關係配置部分,我們註釋了一部分關係約束配置程式碼主要是因為有些關係不能共存大家可以自己去掉執行試試,這部分程式碼希望初學者不要略過。關於上面的程式碼相信大家看註釋都可以明白,這裡我主要強調一點,那就是多重外來鍵約束。大家通過上圖已經看到CheckPerson和DeliverPerson的約束像在Data Annotations中提到的一樣並沒有達到我們的預期,其主要原因是因為EF並沒有明白這種約束關係,解決辦法很簡單,只要配置“Employee”和“Order”一對多約束關係即可(注意只有配置一個屬性即可,例如我們配置CheckPerson):

modelBuilder.Entity<Order>().HasRequired(o => o.CheckPerson).WithMany(emp => emp.CheckOrder).WillCascadeOnDelete();

Fluent API繼承實現

下面看一下EF強大的繼承實現,在EF中支援三種型別的繼承實現:

  • Table-Per-Hierarchy(TPH):EF預設支援的型別,無需配置即可實現,整個層次在一張表中,基類中沒有的屬性被標記為可空,通過新增一個額外的“Discniminator”列進行型別區分;
  • Table-Per-Type(TPT:每個型別一張表,在子類中包含自身屬性和一個指向基類的外來鍵列;
  • Table-Per-Concrete Calss(TPC):每個型別一張表,但是和TPT不同的是子類中並沒有建立外來鍵列而是直接將基類的屬性在子類中展開(子類表包含基類表的所有列);

在演示上面幾種方式之前先讓我們定義兩個類“Worker”表示當前在職員工和“Retired”表示退休員工,它們繼承於“Employee”。

TPH方式

TPH方式是EF預設支援,我們無需更多的配置即可完成。

Worker類:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    public class Worker:Employee
    {
        public decimal AnnualSalary
        {
            get;
            set;
        }
    }
}

Retired類:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodeFirst.Entities
{
    public class Retired:Employee
    {
        public decimal MonthlyPension
        {
            get;
            set;
        }
    }
}

接下來插入兩條資料進行測試:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using CodeFirst;
using CodeFirst.Entities;

namespace CodeFirst
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new OrderContext())
            {
                db.Workers.Add(new Worker()
                {
                    No = 1,
                    Name = new Name { FirstName="Stephen",LastName="Chow"},
                    Title = "Software Architect",
                    BirthDate=new DateTime(1976,10,10),
                    Address="Peking",
                    Note="",
                    CreateDate=DateTime.Now,
                    AnnualSalary=999999999
                });

                db.Retireds.Add(new Retired
                {
                    No = 2,
                    Name = new Name { FirstName = "Jeffrey", LastName = "Lee" },
                    Title = "Software Development Engineer",
                    BirthDate = new DateTime(1956, 8, 8),
                    Address = "Hong Kong",
                    Note = "",
                    CreateDate = DateTime.Now,
                    MonthlyPension=9999999
                });
                db.SaveChanges();
            }
        }
    }
}

下面是具體結果:

TPT方式

使用TPT方式其實也十分簡單,只需要配置基類及子類生成的表資訊即可:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using CodeFirst.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace CodeFirst
{
    public class OrderContext:DbContext
    {
        public OrderContext()
            : base("CodeFirstDb")
        {
            Database.SetInitializer<OrderContext>(
                new DropCreateDatabaseIfModelChanges<OrderContext>()
            );
        }

        public DbSet<Order> Orders
        {
            get;
            set;
        }

        public DbSet<Employee> Employees
        {
            get;
            set;
        }

        public DbSet<Worker> Workers
        {
            get;
            set;
        }

        public DbSet<Retired> Retireds
        {
            get;
            set;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            //modelBuilder.Entity<Employee>().ToTable("People", "Person");//指定“Employee”對應表名及架構

            modelBuilder.Entity<Employee>().HasKey(emp => emp.No);//定義主鍵為“No”
            //modelBuilder.Entity<Employee>().HasKey(emp => new { emp.No, emp.Title });//指定"No"和“Title”作為複合主鍵,使用Data Annotations無法做到

            modelBuilder.Entity<Employee>().Property(emp => emp.No).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);//去掉標識列,前面我們也提到過通過Data Annotations通用可以去掉主鍵預設標示屬性

            modelBuilder.Entity<Employee>().Property(emp => emp.Title).HasMaxLength(30);//指定“Title”最大長度為30

            modelBuilder.Entity<Employee>().Property(emp => emp.BirthDate).IsRequired();//指定“BirthDate”為不可為空

            modelBuilder.Entity<Employee>().Property(emp => emp.Note).HasColumnName("Notes").HasColumnType("ntext");//指定“Note”對應列名為“Notes”,並指定資料型別

            modelBuilder.Entity<Employee>().Ignore(emp => emp.PhotoPath);//指定“PhotoPath”為非對映列

            //modelBuilder.Entity<Customer>().HasRequired(c=>c.Orders).WithMany().Map(m=>m.MapKey("CustomerOrder"));

            modelBuilder.Entity<Employee>().Property(emp => emp.Title).IsUnicode(true);//“Title”列是否支援Unicode編碼

            modelBuilder.ComplexType<Name>().Property(n=>n.FirstName).HasMaxLength(50);//指定Name為複雜資料型別,並指定複雜型別中“FirstName”長度
            //modelBuilder.Entity<Employee>().Property(emp => emp.Name.LastName).HasMaxLength(20);//還可以通過這種方式指定複雜型別“Name”的“LastName”列的長度

            //modelBuilder.Entity<Employee>().Property(emp => emp.Address).IsConcurrencyToken();//指定“Address”進行併發控制,通常這一列我們知道為“TimeStamp”列而不是“Addree”這裡只是為了說明可以標記其他列
            modelBuilder.Entity<Employee>().Property(emp => emp.TimeStamp).IsRowVersion();//通過指定“TimeStamp”進行併發版本控制

            /*下面程式碼演示EF中的繼承關係實現*/
            //TPH預設支援,無需手動進行配置
            //TPT,只需要指定生成的表即可
            modelBuilder.Entity<Employee>().ToTable("People", "Person");
            modelBuilder.Entity<Worker>().ToTable("Worker", "Person");
            modelBuilder.Entity<Retired>().ToTable("Retired", "Person");
        }
    }
}

生成的表結構如下:

TPC方式

最後看一下TPC方式,TPC方式同TPT一樣同樣是每個型別建立一張表,不同的是TPC每個子類中只有子類特有屬性和外來鍵列,子類通過外來鍵查詢基類屬性;而在TPC方式中每個子類和基類之間並沒有建立約束關係,子類表中擁有自身屬性和基類所有屬性。TPC定義方式也很簡單,只需要在子類中通過“MapInheritedProperties”方法指定整合基類屬性即可:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using CodeFirst.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace CodeFirst
{
    public class OrderContext:DbContext
    {
        public OrderContext()
            : base("CodeFirstDb")
        {
            Database.SetInitializer<OrderContext>(
                new DropCreateDatabaseIfModelChanges<OrderContext>()
            );
        }

        public DbSet<Order> Orders
        {
            get;
            set;
        }

        public DbSet<Employee> Employees
        {
            get;
            set;
        }

        public DbSet<Worker> Workers
        {
            get;
            set;
        }

        public DbSet<Retired> Retireds
        {
            get;
            set;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            //modelBuilder.Entity<Employee>().ToTable("People", "Person");//指定“Employee”對應表名及架構

            modelBuilder.Entity<Employee>().HasKey(emp => emp.No);//定義主鍵為“No”
            //modelBuilder.Entity<Employee>().HasKey(emp => new { emp.No, emp.Title });//指定"No"和“Title”作為複合主鍵,使用Data Annotations無法做到

            modelBuilder.Entity<Employee>().Property(emp => emp.No).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);//去掉標識列,前面我們也提到過通過Data Annotations通用可以去掉主鍵預設標示屬性

            modelBuilder.Entity<Employee>().Property(emp => emp.Title).HasMaxLength(30);//指定“Title”最大長度為30

            modelBuilder.Entity<Employee>().Property(emp => emp.BirthDate).IsRequired();//指定“BirthDate”為不可為空

            modelBuilder.Entity<Employee>().Property(emp => emp.Note).HasColumnName("Notes").HasColumnType("ntext");//指定“Note”對應列名為“Notes”,並指定資料型別

            modelBuilder.Entity<Employee>().Ignore(emp => emp.PhotoPath);//指定“PhotoPath”為非對映列

            //modelBuilder.Entity<Customer>().HasRequired(c=>c.Orders).WithMany().Map(m=>m.MapKey("CustomerOrder"));

            modelBuilder.Entity<Employee>().Property(emp => emp.Title).IsUnicode(true);//“Title”列是否支援Unicode編碼

            modelBuilder.ComplexType<Name>().Property(n=>n.FirstName).HasMaxLength(50);//指定Name為複雜資料型別,並指定複雜型別中“FirstName”長度
            //modelBuilder.Entity<Employee>().Property(emp => emp.Name.LastName).HasMaxLength(20);//還可以通過這種方式指定複雜型別“Name”的“LastName”列的長度

            //modelBuilder.Entity<Employee>().Property(emp => emp.Address).IsConcurrencyToken();//指定“Address”進行併發控制,通常這一列我們知道為“TimeStamp”列而不是“Addree”這裡只是為了說明可以標記其他列
            modelBuilder.Entity<Employee>().Property(emp => emp.TimeStamp).IsRowVersion();//通過指定“TimeStamp”進行併發版本控制

            /*下面程式碼演示EF中的繼承關係實現*/
            //TPH預設支援,無需手動進行配置
            //TPT,只需要指定生成的表即可
            //modelBuilder.Entity<Employee>().ToTable("People", "Person");
            //modelBuilder.Entity<Worker>().ToTable("Worker", "Person");
            //modelBuilder.Entity<Retired>().ToTable("Retired", "Person");
            //TPC,只要子類中指定對映繼承屬性即可
            modelBuilder.Entity<Employee>().ToTable("People", "Person");
            modelBuilder.Entity<Worker>().Map(m => {
                m.MapInheritedProperties();
                m.ToTable("Worker", "Person");
            });
            modelBuilder.Entity<Retired>().Map(m =>
            {
                m.MapInheritedProperties();
                m.ToTable("Retired", "Person");
            });
        }
    }
}

生成的表結構如下:

clip_image012

注意:儘管Fluent API功能更加強大,對於可以使用Data Annotations實現的功能我們推薦使用Data Annotations方式。

至此關於EF中預設約定及自定義配置的內容已經討論結束了,關於EF中如何進行查詢以及如何優化查詢等更多內容敬請關注後面的文章。

 
 
 
 
 

相關文章