很感謝王胖子2012同學的具體建議,從這次日記開始,我會在日記的開頭介紹一下這篇日記的主要內容並給代碼加高亮顯示。
好的,讓我們切入正題。這篇日記我將介紹Code First將類之間的引用關系映射為數據表之間的一對多關系的默認規則。主要包含以下兩部分內容:
1.Code First將類之間的引用關系映射為數據表之間一對多關系的默認規則。
2.用Fluent API更改外鍵的nullable屬性和外鍵的名字。
3.用Fluent API建立兩個一對多數據表之間的多個外鍵。
4.用Fluent API設置級聯刪除功能。
1. Code First處理一對多關系的默認規則
在詳細介紹Code First的默認規則之前,先讓我們看兩個示例,一個是一對多關系,另一個是多對多關系。
我在這個系列的日記中使用的示例是一個簡單的訂單管理系統。
在這個訂單管理的業務中,我們有訂單和訂單條目兩個實體。它們之間存在着一對多的關系;一個訂單包含多個條目,一個條目只屬於一個訂單。
根據我們的業務邏輯我們建立了如下的兩個類:
訂單類:
public class Order { public int OrderId { get; set; } public DateTime CreatedDate { get; set; } public Customer Customer { get; set; }
public List<OrderItem> OrderItems { get; set; }
public Order() { OrderItems = new List<OrderItem>(); } public void AddNewOrderItem(Product product, decimal retailPrice) { var item = new OrderItem(); item.Products = new List<Product>() { product }; item.RetailPrice = retailPrice; item.Order = this; OrderItems.Add(item); } public bool HasBuy(Product product) { bool hasBuy = false; foreach (var item in OrderItems) { if (item.Products.Count > 0 && item.Products[0].Catalog.ProductCatalogId == product.Catalog.ProductCatalogId) { hasBuy = true; break; } } return hasBuy; } public void MergeOrderItem(Product product, decimal retailPrice, bool canMergeIfDifferencePrice) { foreach (var item in OrderItems) { if (item.Products.Count > 0 && item.Products[0].Catalog.ProductCatalogId == product.Catalog.ProductCatalogId) { if (item.RetailPrice != retailPrice && canMergeIfDifferencePrice == false) { throw new Exception("Can not merge items because they have different retail price"); } item.RetailPrice = retailPrice; item.Products.Add(product); } } } }
訂單條目類:
public class OrderItem { public int OrderItemId { get; set; }
public Order Order { get; set; }
public List<Product> Products { get; set; } public decimal RetailPrice { get; set; } public OrderItem() { Products = new List<Product>(); } }
大家可以注意一下在這兩個類中標識為紅顏色的部分,在訂單類中有一個訂單條目的集合,在訂單條目類中有一個訂單類的引用。
如果兩個類互相包含另一個的實例或實例的集合,那么Code First就會默認為這兩個表之間有一對多的關系,包含實例集合的類為主表,包含單個實例的類為子表。
我們修改一下自定義的可以插入基礎數據的DropCreateOrderDatabaseWithSeedValueAlways類,插入一些產品目錄,產品和客戶的基礎數據。
protected override void Seed(OrderSystemContext context) { context.ProductCatalogs.Add(new ProductCatalog { CatalogName = "DELL E6400", Manufactory = "DELL", ListPrice = 5600, NetPrice = 4300 }); context.ProductCatalogs.Add(new ProductCatalog { CatalogName = "DELL E6410", Manufactory = "DELL", ListPrice = 6500, NetPrice = 5100 }); context.ProductCatalogs.Add(new ProductCatalog { CatalogName = "DELL E6420", Manufactory = "DELL", ListPrice = 7000, NetPrice = 5400 }); context.Products.Add(new Product{ Catalog = new ProductCatalog { CatalogName = "DELL E6400", Manufactory = "DELL", ListPrice = 5600, NetPrice = 4300 }, CreateDate=DateTime.Parse("2010-1-20"), ExpireDate = DateTime.Parse("2013-1-20")}); context.Customers.Add(new Customer{IDCardNumber = "120104198106072518", CustomerName = "Alex", Gender = "M", PhoneNumber = "test" ,Address = new Address{Country = "China", Province = "Tianjin", City = "Tianjin", StreetAddress = "Crown Plaza", ZipCode = "300308" }}); }
然后我們可以寫一個單元測試方法來測試一下Code First會建立怎樣的一對多關系。
[TestMethod] public void CanAddOrder() { OrderSystemContext unitOfWork = new OrderSystemContext(); ProductRepository productRepository = new ProductRepository(unitOfWork); OrderRepository orderRepository = new OrderRepository(unitOfWork); CustomerRepository customerRepository = new CustomerRepository(unitOfWork); Order order = new Order { CreatedDate = DateTime.Now, Customer = customerRepository.GetCustomerById("120104198106072518") }; order.AddNewOrderItem(productRepository.GetProductByCatalog(new ProductCatalog { ProductCatalogId = 1 }), 5100); orderRepository.AddNewOrder(order); unitOfWork.CommitChanges(); }
我們可以打開數據庫,Code First默認會在OrderItems表中建立一個到Orders表的外鍵。外鍵列的名字是主表類的類名+”_”+主表類中主鍵屬性的名字。我們后邊會介紹如何改變默認的命名規則。
其實在兩個類中,只要有一個類包含了另一個類的實例,Code First就可以按照為我們建立數據表之間的一對多關系。我們可以修改一下OrderItem類,將其中包含的Order類的實例注釋掉
public class OrderItem { public int OrderItemId { get; set; } //public Order Order { get; set; } public List<Product> Products { get; set; } public decimal RetailPrice { get; set; } public OrderItem() { Products = new List<Product>(); } }
我們可以重新執行一下我們的單元測試程序,我們可以看到Code First為我們建立的一對多關系是完全一樣的。
如果我們僅僅保留OrderItem對Order的引用,那么我們也會得到同樣的數據庫schema,但是因為這種實現方式不符合我們的業務邏輯並且與之前的實現方法建立的數據庫都是一樣的,我在這里就不詳細介紹了。
2 用Fluent API更改外鍵的nullable屬性和外鍵的名字
我們從上面的數據庫結構中可以看出,Code First默認為我們建立的外鍵是可以為null的,但是按照我們的業務邏輯,OrderItem是屬於某個Order的,不可能存在單獨的OrderItem。我們可以通過Fluent API使數據庫的結構和我們的業務需求一致。我們可以按照以前使用Fluent API進行配置時使用的方法,定義一個繼承了EntityTypeConfiguration泛型類的子類對Order類相關的數據庫映射進行配置。
public class OrderEntityConfiguration: EntityTypeConfiguration<Order> { public OrderEntityConfiguration() { this.HasMany(order => order.OrderItems).WithRequired(item => item.Order); } }
HasMany表示一個Order包含多個OrderItem。WithRequired表示OrderItem類必須包含一個不為null的Order類的實例。和WithRequired類似的還有兩個方法,WithOptional和WithMany.WithOptional表示OrderItem類可以包含一個Order類的實例或是null。WithMany表示OrderItem應該包含Order類實例的集合。根據我們的業務要求,我們肯定需要用WithRequired.
其實和With的系列方法類似,Has也有三個方法:HasMany,HasRequired,HasOptional.
HasMany表示Order類應該包括OrderItem實例的集合;HasRequired表示Order類應該包括OrderItem的一個不為null的實例;HasOptional表示Order類應該包括OrderItem的一個實例或是null。
我們將對Order類的映射配置加入到DbContext子類的配置集合中:
modelBuilder.Configurations.Add(new OrderEntityConfiguration());
我們重新執行一下我們的單元測試程序,可以得到不一樣的數據庫結構
我們還可以通過Fluent API設置外鍵的名字。我們可以通過在Has…With…方法設置主外鍵關系之后調用Map方法,設置外鍵的名字:
this.HasMany(order => order.OrderItems).WithRequired(item => item.Order).Map(o => o.MapKey("OrderId"));
我們重新執行一下單元測試程序,發現OrderItem表中外鍵的名字已經變為我們設置的OrderId
3.用Fluent API建立兩個一對多數據表之間的多個外鍵
由於我們的業務需求發生了改變,需要記錄每個訂單是由哪個銷售人員創建的以及由哪個銷售人員批准的,於是我們就需要創建一個銷售人員的類:
public class SalesPerson { public string EmployeeID { get; set; } public string Name { get; set; } public string Gender { get; set; } public DateTime HiredDate { get; set; } public List<Order> CreatedOrders { get; set; } public List<Order> ApprovedOrders { get; set; } }
在這個類中有兩個Order的集合,一個是銷售人員創建的訂單,一個是銷售人員批准的訂單。我們還需要在訂單類中加兩個屬性,表示訂單是由哪個銷售人員創建的以及由哪個銷售人員批准的。
public SalesPerson CreatedBy { get; set; } public SalesPerson ApprovedBy { get; set; }
我們使用Fluent API對SalesPerson類的數據庫映射進行了配置。
public class SalesPersonValueObjectConfiguration: EntityTypeConfiguration<SalesPerson> { public SalesPersonValueObjectConfiguration() { HasKey(p => p.EmployeeID).Property(p => p.EmployeeID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); Property(p => p.Name).IsRequired().HasMaxLength(100); Property(p => p.Gender).IsRequired().HasMaxLength(1); } }
然后將SalesPerson類的配置加入到Code First的配置集合中。
modelBuilder.Configurations.Add(new SalesPersonValueObjectConfiguration());
我們重新執行一下我們的單元測試程序,大家可以發現Code First根本無法正確地建立表之間的主外鍵關系。
因為在這兩個表之間存在多個一對多關系,所以Code First無法處理這種情況。所以我們必須用Fluent API幫助Code First建立表之間的主外鍵關系。
public class SalesPersonValueObjectConfiguration: EntityTypeConfiguration<SalesPerson> { public SalesPersonValueObjectConfiguration() { HasKey(p => p.EmployeeID).Property(p => p.EmployeeID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); Property(p => p.Name).IsRequired().HasMaxLength(100); Property(p => p.Gender).IsRequired().HasMaxLength(1); HasMany(p => p.CreatedOrders).WithOptional(o => o.CreatedBy).Map(p => p.MapKey("CreatedBy")); HasMany(p => p.ApprovedOrders).WithOptional(o => o.ApprovedBy).Map(p => p.MapKey("ApprovedBy")); } }
我們重新執行我們的測試程序,我們發現這次建立的一對多關系是正確的:
4.用Fluent API設置級聯刪除功能
如果兩個表之間存在一對多關系,Code First默認會開啟兩個表之間的級聯刪除功能。我們可以寫一個測試方法來測試級聯刪除是不是默認的行為。
[TestMethod] public void CanAddAndDeleteOrder() { OrderSystemContext unitOfWork = new OrderSystemContext(); ProductRepository productRepository = new ProductRepository(unitOfWork); OrderRepository orderRepository = new OrderRepository(unitOfWork); CustomerRepository customerRepository = new CustomerRepository(unitOfWork); Order order = new Order { CreatedDate = DateTime.Now, Customer = customerRepository.GetCustomerById("120104198106072518") }; order.AddNewOrderItem(productRepository.GetProductByCatalog(new ProductCatalog { ProductCatalogId = 1 }), 5100); orderRepository.AddNewOrder(order); unitOfWork.CommitChanges(); orderRepository.DeleteOrder(1); unitOfWork.CommitChanges(); }
我們這段代碼只刪除了訂單,但是如果我們打開數據庫,查詢訂單和訂單條目表我們會發現這兩個表中的數據都被刪除掉了。
我們可以通過WillCascadeOnDelete方法將級聯刪除功能關閉掉。
this.HasMany(order => order.OrderItems) .WithRequired(item => item.Order) .Map(o => o.MapKey("OrderId")) .WillCascadeOnDelete(false);
我們這次筆記介紹了如何映射一對多關系的細節,下一次的日記我將介紹多對多關系。