Entity Framework 4.1 Code-First 學習筆記
- 2019 年 10 月 7 日
- 筆記
CodeFirst提供了一種先從代碼開始工作,並根據代碼直接生成數據庫的工作方式。Entity Framework 4.1在你的實體不派生自任何基類、不添加任何特性的時候正常的附加數據庫。另外呢,實體的屬性也可以添加一些標籤,但這些標籤不是必須的。下面是一個簡單的示例:
publicclass Order { publicint OrderID { get; set; } publicstring OrderTitle { get; set; } publicstring CustomerName { get; set; } public DateTime TransactionDate { get; set; } public List<OrderDetail> OrderDetails { get; set; } } publicclass OrderDetail { publicint OrderDetailID { get; set; } publicint OrderID { get; set; } publicdecimal Cost { get; set; } publicstring ItemName { get; set; } public Order Order { get; set; } } publicclass MyDomainContext : DbContext { public DbSet<Order> Orders { get; set; } public DbSet<OrderDetail> OrderDetails { get; set; } static MyDomainContext() { Database.SetInitializer<MyDomainContext>( new DropCreateDatabaseIfModelChanges<MyDomainContext>()); } }
上面的示例可以看出,Order類和OrderDetail類沒有派生自任何基類,也沒有附加EF特性,在將它們添加到上下文(上下文需要派生自DbContext)中時,會自動生成相應的數據表。唯一與EF相關的類MyDomainContext是必須的,它用來提供數據的上下文支持,它可以和Order、OrderDetail類不在同一個應用程序集中。
context 必須滿足下面的要求:
- 派生自 System.Data.Entity.DbContext
- 對於你希望使用的每一個實體集定義一個屬性
- 每一個屬性的類型是 System.Data.Entity.DbSet<T>,T 就是實體的類型
- 每一個屬性都是讀寫屬性 read/write ( get/set )
在這裡,DbContext 基類通過反射來獲取映射到數據庫的實體。這遵循一系列的約定。
例如,對於 Order 來說,他的屬性 OrderID 必須是主鍵,其它的約定將用來推斷列名和列的類型,默認數據庫中的列名是屬性名,使用 string 類型來影射數據庫中的 nvarchar(128), 如果屬性的類型是可空的,那麼,影射到數據庫中的允許 NULL 等等。以後我們可以看到如果覆蓋這些約定。
我們將增加一個靜態的構造函數,這個靜態的構造函數對於整個應用程序域來說建立一個標準,當數據庫的上下文初始化的時候,檢查數據庫的架構是否與模型相符,如果不是的話,將刪除數據庫然後重新創建它。EF 將會創建一個名為 dbo.EdmMetadata 的表,然後將模型結構的 Hash 保存到其中來實現。
如果數據庫不存在,EF 將會創建它,創建什麼數據庫呢?默認情況下,將在你的本地機器上,使用上下文對象名稱,有許多方式來覆蓋這個行為,最簡單的方式是在配置文件中增加一個名字為上下文對象名稱的數據庫連接串,在我這裡,叫做 MyDomainContext,還可以通過實現一個構造函數,然後調用非默認的基類構造函數來實現。
—————————————————————————-
覆蓋默認約定有兩種方式:
- 攔截模型的構建器,使用Fluent API 來修改模型
- 為我們的模型增加標籤
通過構建器來覆蓋默認約定,我們需要重寫 DbContext 的一個方法 OnModelCreating:
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); //Map schemas modelBuilder.Entity<Order>().ToTable("efdemo.Order"); modelBuilder.Entity<Order>().Property(x => x.OrderID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) .IsRequired() .HasColumnName("TheOrderID"); //String columns modelBuilder.Entity<Order>().Property(x => x.OrderTitle) .IsRequired() .HasMaxLength(64); }
這段代碼先執行父類的OnModelCreating方法,然後將Order類映射到efdemo架構Order表中,再然後為OrderID設置規則,規定它為標識列,自增,不能為空,且映射到表中的TheOrderID列上面。對於String類型的數據列,還可以指定數據的長度。
使用標註來覆蓋默認約定,這種方式需要的代碼量比較小,且表現的更加自然:
publicclass Order { publicint OrderID { get; set; } [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] publicint OrderNumber { get; set; } [Required] [StringLength(32, MinimumLength =2)] publicstring OrderTitle { get; set; } [Required] [StringLength(64, MinimumLength =5)] publicstring CustomerName { get; set; } [Required] public DateTime TransactionDate { get; set; } }
在上面的這段代碼中,我們強制了OrderNumber為主鍵列,且為自增;OrderTitle為不能為空且最大長度為32,最小長度為2,儘管我們如此規定,但最小長度是不會被映射到數據表中的,這一點可以理解,最小長度會在數據存儲時進行驗證,如果小於2將會拋出異常,無法完成保存。
如何在兩種覆蓋默認約定的方法中進行選擇呢?我們的原則是:使用標註來豐富模型的驗證規則;使用 OnModelCreated 來完成數據庫的約束(主鍵,自增長,表名,列類型等等)。
—————————————————————————-
關於數據的加載,在默認情況下, EF4.1 僅僅加載查詢中涉及的實體,但是它支持兩種特性來幫助你控制加載:貪婪加載和延遲加載。
使用貪婪加載方式獲取數據集的代碼如下:
var orders = from o in context.Orders.Include("OrderDetails").Include("Businesses") where o.CustomerName =="Mac" select o;
這段代碼在加載Orders的時候,會將OrderDetails信息和Business信息也加載到orders中。這樣的查詢會引起效率問題,容易使程序性能變差。鑒於性能問題,EF4.1還支持一種延遲加載的數據加載方式,默認情況下,延遲加載是被支持的,如果你希望禁用它,必須顯式聲明,最好的位置是在 DbContext 的構造器中:
public MyDomainContext() { this.Configuration.LazyLoadingEnabled =false; }
當禁用了延遲加載以後,當查詢一個實體集的時候,相關的子實體也一併加載。當 EF 訪問實體的子實體的時候是如何工作的呢?你的集合是 POCO 的集合,所以,在訪問的時候沒有事件發生,EF 通過從你定義的實體派生一個動態的對象,然後覆蓋你的子實體集合訪問屬性來實現。這就是為什麼需要標記你的子實體集合屬性為 virtual 的原因。
publicclass Order { publicint OrderID { get; set; } publicstring OrderTitle { get; set; } publicstring CustomerName { get; set; } public DateTime TransactionDate { get; set; } publicvirtual List<OrderDetail> OrderDetails { get; set; } publicvirtual List<Business> Businesses { get; set; } }
貪婪加載:減少數據訪問的延遲,在一次數據庫的訪問中返回所有的數據;你需要知道你將作什麼,並且顯式聲明。延遲加載:非常寬容,因為只在需要的時候加載數據,不需要預先計劃;可能因為數據訪問的延遲而降低性能,考慮到每訪問父實體的子實體時,就需要訪問數據庫。兩種方式各有優缺點,該怎麼選擇呢?除非需要循環中加載數據,我使用延遲加載。這樣的話,可能會造成2-3 次服務器的查詢,但是仍然是可以接受的,特別是考慮到貪婪加載的效率問題
—————————————————————————-
默認情況下,EF4.1 將類映射到表,這是約定,但是有時候,我們需要模型比表的粒度更細一些。地址是一個典型的例子,看一下下面的客戶類:
publicclass Client { publicint ClientID { get; set; } [Required] [StringLength(32, MinimumLength=2)] publicstring ClientName { get; set; } public Address ResidentialAddress { get; set; } public Address DeliveryAddress { get; set; } } publicclass Address { [Required] publicint StreetNumber { get; set; } [Required] [StringLength(32, MinimumLength=2)] publicstring StreetName { get; set; } }
在上面的例子代碼中,Client類的兩個Address屬性會被映射到表Address中,如果我們希望將Address都映射到一個表中,將地址展開,這需要使用複雜類型,通過構造器來覆蓋默認約定,代碼如下:
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Client>().Property(x => x.ClientID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); modelBuilder.ComplexType<Address>(); modelBuilder.Entity<Client>().Property(i => i.ResidentialAddress.StreetNumber).HasColumnName("ResStreetNumber"); modelBuilder.Entity<Client>().Property(i => i.ResidentialAddress.StreetName).HasColumnName("ResStreetName"); modelBuilder.Entity<Client>().Property(i => i.DeliveryAddress.StreetNumber).HasColumnName("DelStreetNumber"); modelBuilder.Entity<Client>().Property(i => i.DeliveryAddress.StreetName).HasColumnName("DelStreetName"); }
首先,我指定 client-id 作為自動增長的標識列。然後,指定 Address 是複雜類型。如果願意的話,也可以將 [ComplexType] 標籤加到類上來說明。然後,使用 Lambda 表達式將每一個子屬性映射到列上,這將會生成如下的表。模型的使用:
using (var context1 =new MyDomainContext()) { var client =new Client { ClientName ="Joe", ResidentialAddress =new Address { StreetNumber =15, StreetName ="Oxford" }, DeliveryAddress =new Address { StreetNumber =514, StreetName ="Nolif" } }; context1.Clients.Add(client); context1.SaveChanges(); } using (var context2 =new MyDomainContext()) { var clients = from w in context2.Clients where w.ClientName =="Joe" select w; foreach (var client in clients) { Console.WriteLine("client residential StreetNumber: "+ client.ResidentialAddress.StreetNumber); Console.WriteLine("client residential StreetName: "+ client.ResidentialAddress.StreetName); Console.WriteLine("client delivery StreetNumber: "+ client.DeliveryAddress.StreetNumber); Console.WriteLine("client delivery StreetName: "+ client.DeliveryAddress.StreetName); } }
對於複雜類型,最值得注意的是空的管理。即使複雜類型的所有屬性都是可空的,你也不能將整個複雜類型的對象設為 null, 例如,在這種情況下,即使街道的名稱和街道的號碼不是必填的,也不能有一個住宅的地址為 null,需要創建一個所有屬性都是 null 的地址對象來表示。同樣的道理,當你獲取一個實體的時候,即使所有的屬性都是 null ,EF4.1 也將會創建一個複雜類型的對象。
—————————————————————————-
在通常的業務環境中,我們需要處理多對多的關係,例如,一個訂單都有哪些員工參與,一個員工參與過哪些訂單,這就需要在原有的訂單類中加入員工的實體列表,並在員工實體中加入訂單的實體列表。相應的實體代碼如下:
publicclass Order { publicint OrderID { get; set; } [Required] [StringLength(32, MinimumLength =2)] publicstring OrderTitle { get; set; } [Required] [StringLength(64, MinimumLength=5)] publicstring CustomerName { get; set; } public DateTime TransactionDate { get; set; } publicbyte[] TimeStamp { get; set; } publicvirtual List<OrderDetail> OrderDetails { get; set; } publicvirtual List<Employee> InvolvedEmployees { get; set; } } publicclass Employee { publicint EmployeeID { get; set; } publicstring EmployeeName { get; set; } publicvirtual List<Order> Orders { get; set; } }
有了這段代碼,EF就會為我們創建一個訂單與員工的對應關係表(OrderEmployee),這張表中有兩個字段:員工ID(Employee_EmployeeID)與訂單ID(Order_OrderID)。這是EF的默認約定,如果要修改關係表的名稱,並修改對應的字段的名稱,我們可以使用下面的代碼來完成:
modelBuilder.Entity<Employee>() .HasMany(e => e.Orders) .WithMany(e => e.InvolvedEmployees) .Map(m => { m.ToTable("EmployeeOrder"); m.MapLeftKey("EmployeeID"); m.MapRightKey("OrderID"); });
通過這段代碼,還可以控制沒有映射到表的類。下面我們來測試這個模型:
using (var context =new MyDomainContext()) { var order =new Order { OrderTitle ="Pens", CustomerName ="Mcdo』s", TransactionDate = DateTime.Now, InvolvedEmployees =new List<Employee>() }; var employee1 =new Employee { EmployeeName ="Joe", Orders =new List<Order>() }; var employee2 =new Employee { EmployeeName ="Black", Orders =new List<Order>() }; context.Orders.Add(order); order.InvolvedEmployees.Add(employee1); order.InvolvedEmployees.Add(employee2); context.SaveChanges(); }
在這個例子中,我甚至都沒有在數據上下文中將僱員加入到僱員的集合中,因為他們被引用到訂單的集合中,EF 幫我們完成了。
—————————————————————————-
通常情況下,我們的業務環境需要有並發的處理。對於悲觀的並發處理,需要加入記錄鎖的機制,隨之而來帶來一些問題,例如,在自動釋放鎖之前,系統應該鎖定多長的時間;樂觀並發要簡單一些,樂觀並發假定用戶的修改很少衝突,我們要在記錄中加入數據行的版本號,當用戶保存記錄的時候,通過驗證版本號,如果版本號一致,則驗證通過,進行保存,如果版本號不一致,則拒絕保存。
在 EF 中,這被稱為並發標識 concurrenty token,在這篇文章中,我使用 SQL Server 的 time-stamp 特性,這需要在表中增加一個 time-stamp 類型的列,我們通過它來實現樂觀並發。由 SQL Server 在每次記錄被更新的時候維護這個列。為了告訴 EF 在實體中有一個屬性表示並發標識,你可以通過標籤 [ConcurrencyCheck] 來標識這個屬性,或者使用模型構建器。我認為並發標識定義了業務規則,應該是模型的一部分。所以這裡使用標籤。相應的模型代碼如下:
publicclass Order { publicint OrderID { get; set; } [Required] [StringLength(32, MinimumLength =2)] publicstring OrderTitle { get; set; } [Required] [StringLength(64, MinimumLength=5)] publicstring CustomerName { get; set; } public DateTime TransactionDate { get; set; } [ConcurrencyCheck] [Timestamp] publicbyte[] TimeStamp { get; set; } publicvirtual List<OrderDetail> OrderDetails { get; set; } publicvirtual List<Employee> InvolvedEmployees { get; set; } }
在這段代碼中,當我們通過 DbContext 調用 SaveChanges 的時候,將會使用樂觀並發。Timestamp 屬性的類型是 byte[], 通過標籤 Timestamp ,將這個屬性映射到 SQL Server 的 time-stamp 類型的列。
—————————————————————————-
在 ORM 文獻中,有三種方式將對象的繼承關係映射到表中。
- 每個類型一張表 TPT: 在繼承層次中的每個類都分別映射到數據庫中的一張表,彼此之間通過外鍵關聯。
- 繼承層次中所有的類型一張表 TPH:對於繼承層次中的所有類型都映射到一張表中,所有的數據都在這張表中。
- 每種實現類型一張表 TPC: 有點像其他兩個的混合,對於每種實現類型映射到一張表,抽象類型像 TPH 一樣展開到表中。
這裡我將討論 TPT 和 TPH,EF 的好處是可以混合使用這些方式。
為了模擬實際的業務需求,我定義了一個簡單的繼承層次,一個抽象基類和兩個派生類,代碼如下:
publicabstractclass PersonBase { publicint PersonID { get; set; } [Required] publicstring FirstName { get; set; } [Required] publicstring LastName { get; set; } publicint Age { get; set; } } publicclass Worker : PersonBase { publicdecimal AnnualSalary { get; set; } } publicclass Retired : PersonBase { publicdecimal MonthlyPension { get; set; } }
使用TPT方式:我們需要告訴構造器如何創建表:
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<PersonBase>().HasKey(x => x.PersonID); modelBuilder.Entity<PersonBase>().Property(x => x.PersonID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); // TPT mapping modelBuilder.Entity<PersonBase>().ToTable("tpt.Person"); modelBuilder.Entity<Worker>().ToTable("tpt.Worker"); modelBuilder.Entity<Retired>().ToTable("tpt.Retired"); }
使用TPH方式:TPH 是 EF 實際上默認支持的。我們可以簡單地注釋到前面例子中的對錶的映射來使用默認的機制。
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<PersonBase>().HasKey(x => x.PersonID); modelBuilder.Entity<PersonBase>().Property(x => x.PersonID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); // TPT mapping //modelBuilder.Entity<PersonBase>().ToTable("tpt.Person"); //modelBuilder.Entity<Worker>().ToTable("tpt.Worker"); //modelBuilder.Entity<Retired>().ToTable("tpt.Retired"); }
結果是現在使用一張表來影射整個的繼承層次。整個的層次被展開到一張表中,基類中沒有的屬性被自動標記為可空。還有一個額外的區分列,用來保存數據是屬於哪一個類,當 EF 讀取一行的時候,區分列被 EF 用來知道應該創建實例的類型,因為現在所有的類都被映射到了一張表中。
混合使用 TPH 和 TPT:我定義了 Worker 的兩個子類,我希望將這兩個類和 Worker 基類映射到一張表:
publicclass Manager : Worker { publicint? ManagedEmployeesCount { get; set; } } publicclass FreeLancer : Worker { [Required] publicstring IncCompanyName { get; set; } }
注意:每一個屬性都必須是可空的。這在 TPH 中非常不方便,現在我們使用模型構建器來完成。
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<PersonBase>().HasKey(x => x.PersonID); modelBuilder.Entity<PersonBase>().Property(x => x.PersonID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); // TPT mapping modelBuilder.Entity<PersonBase>().ToTable("tpt.Person"); modelBuilder.Entity<Retired>().ToTable("tpt.Retired"); // TPH mapping modelBuilder.Entity<Worker>() .Map<FreeLancer>(m => m.Requires(f => f.IncCompanyName).HasValue()) .Map<Manager>(m => m.Requires(ma => ma.ManagedEmployeesCount).HasValue()) .ToTable("tph.Worker"); }
—————————————————————————-
像所有優秀的框架一樣,EF 知道它並不能優秀到覆蓋所有的角落,通過允許直接訪問數據庫,EF 支持開放底層的 ADO.NET 框架。EF開放了三個API支持直接查詢:
DbContext.Database.ExecuteSqlCommand:這是一個典型的ADO.NET的Command對象,不做解釋。
DbContext.Database.SqlQuery:這個方法將返回的數據集映射到相應的對象,而不去管這個對象是不是實體。重要的是 EF 不會跟蹤返回的對象,即使他們是真正的實體對象。
DbSet.SqlQuery:這個方法返回的實體將會被 EF 跟蹤修改,所以,如果你在這些返回的實體上做了修改,當 DbContext.SaveChanges 被調用的時候,將會被處理。從另一個方面來說,也不能覆蓋列的映射。
另外一個 EF 映射管理的方法是使用 Entity SQL,這種方式是 EF 將實體模型轉換為物理模型,然後將Linq查詢添加到物理模型中,最後將物理模型轉換為數據庫存儲的查詢。舉例來說,我們可以不在DbContext中定義,而獲得我們需要的實體集:
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<SimpleEntry>().HasEntitySetName("MyEntry"); modelBuilder.Entity<SimpleEntry>().ToTable("MyEntry", "man"); modelBuilder.Entity<SimpleEntry>() .Property(s => s.ID) .HasColumnName("SimpleEntryID"); modelBuilder.Entity<SimpleEntry>() .Property(s => s.Name) .HasColumnName("SimpleEntryName"); }
然後,我們將查詢方法暴漏出來:
public IEnumerable<SimpleEntry> GetSimpleEntries() { IObjectContextAdapter adapter =this; var entries = adapter.ObjectContext.CreateQuery<SimpleEntry>("SELECT VALUE MyEntry FROM MyEntry"); return entries; }
這裡使用了ObjectContext進行查詢,和直接使用Sql進行查詢的優勢在於,我們可以在 LINQ 之上進行查詢,最終進行查詢的 SQL 是經過合併的。因此,我們可以通過從一個返回任何結果的簡單查詢開始,然後在其上應用 LINQ來得到有效的查詢,而不需要在使用方查詢整個表。
現在,如果你希望能夠截獲實體的 Insert, Update, 和 Delete 操作,就要靠你自己了。你需要重寫 DbContext.SaveChanges ,獲取特定狀態的實體,實現自己的數據操作邏輯來保存修改,然後在調用 base.SaveChanges 之前將這些實體的狀態切換到 Unmodified 。這可以用,但這是一種特殊的技巧。