Entity Framework Code First 学习日记(6)-一对多关系
很感谢王胖子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);
我们这次笔记介绍了如何映射一对多关系的细节,下一次的日记我将介绍多对多关系。