持久化order涉及对多个实体的操作。尽管只更新order和它的details,但是order的customer和details中的products作为只读数据同样会被涉及到。因为涉及到多个实体,我们就要谈谈实体关系图(entities graph)或者对象关系图(objects graph)。
当保存order时,持久化过程就会被触发,下面是你必须要做的:
根据选择的是外键关联还是独立关联,执行这些步骤的代码会有所不同,但是基本的理念都是相同的。下面我们依次看看这两种情况。
使用外键关联持久化Added状态的实体关系图
使用外键关联customerh和order非常简单,只需设置外键属性为关联实体的主键属性。例如,设置order的CustomerId属性为1,就会自动关联order和ID为1的customer。
前面已经提到,EF不支持部分加载的对象关系图,当order是Added状态时,details也必须是Added。看下面的清单:
清单7.7 使用外键关联创建一个订单
var order = new Order { CustomerId = 1, OrderDate = DateTime.Now.Date }; order.ShippingAddress = new AddressInfo() { Address = "2th street", City = "New York", Country = "USA", ZipCode = "0000001" }; var detail1 = new OrderDetail() { ProductId = 2, Quantity = 3, UnitPrice = 10 }; var detail2 = new OrderDetail() { ProductId = 1, Quantity = 5, UnitPrice = 10 }; order.OrderDetails.Add(detail1); order.OrderDetails.Add(detail2); ctx.Orders.AddObject(order); ctx.SaveChanges();
外键使用起来简单,但也不是唯一维持实体间关系的方式。在model设计期间,你可能不使用外键,而是使用独立关联代替。又或者是你的程序是从EF1.0升级为EF4.0的,EF1.0没有外键的概念。
使用独立关联持久化Added状态的实体关系图
使用独立关联时,Order表的CustomerId列被映射到Customer类的CompanyId属性。在Order类中没有CustomerId属性。结果,关联customer和一个order,必须创建一个customer的实例并且将它和order关联。关联details和它们的product也是如此。
因为只需要customer的ID和pruduct的ID,代替了从数据库检索它们,你可以为它们每一个都创建一个实例,然后将实例和order以及details关联起来。看下面的清单:
清单7.8 使用独立关联创建一个订单
var cust = new Customer() { CompanyId = 1 }; var product1 = new Product() { ProductId = 1 }; var product2 = new Product() { ProductId = 2 }; var order = new Order { Customer = cust, OrderDate = DateTime.Now.Date }; order.ShippingAddress = new AddressInfo() { Address = "2th street", City = "New York", Country = "USA", ZipCode = "0000001" }; var detail1 = new OrderDetail() { Product = product1, Quantity = 3, UnitPrice = 10 }; var detail2 = new OrderDetail() { Product = product2, Quantity = 5, UnitPrice = 10 }; order.OrderDetails.Add(detail1); order.OrderDetails.Add(detail2); ctx.Orders.AddObject(order); ctx.SaveChanges();
上面的代码会工作吗?答案是:No。在持久化时会引发UpdateException异常,因为不能插入null值到Company的Name列。引起这个异常是由于AddObject方法customer被标记为了Added,所以持久化过程也尝试插入customer。因为只设置了customer的ID,name是null,而数据库不允许name为null。
要想工作正常,必须在调用SaveChanges方法之前使用ChangeObjectState方法标记customer为Unchanged。这阳就只有order和它的details被持久化,其他的保持不变。
清单7.9 使用独立关联正确的创建一个订单
ctx.Orders.AddObject(order); osm.ChangeObjectState(cust, EntityState.Unchanged); osm.ChangeObjectState(product1, EntityState.Unchanged); osm.ChangeObjectState(product2, EntityState.Unchanged); ctx.SaveChanges();
如你所见,独立关联操作起来有点难度。
还有一个问题,创建订单时不能关联customer,因为customer还没创建呢。我们不想回到customer表单创建了customer再回来创建订单,而是在同一个表单里创建customer和order。
持久化不同状态的实体关系图
解决上面的问题很简单。可以创建一个完整的Customer实例,然后将它和order关联起来,并且不设置它的状态为Unchanged。
如果在一个方法内部,可能需要一个flag指定customer是新旧与否。如果使用外部方法,需要传这个flag。但是更独立的方案会更好。问题是如何在没有flag的情况下确定customer需要插入数据库与否。
ID是key。在外键关联情况下,如果外键属性是0,customer就是新的;否则customer已经存在。在独立关联情况下,如果Customer实体的ID为0,customer是新的;否则,customer已经存在。
问题又来了。假设写错了shipping address,现在需要修改它。这个很简单,这不涉及关系图,因为只需更新order,不用管它的details。再假设customer需要更多的red shirt(ProductId 1),不要shoes(ProductId 1)以及一个新的green shirt(ProductId 3)。更糟糕的是,订单关联了错误的customer。
这有一点难度。Order实例中的数据保持不变,但是关联的customer发生了变化。details中的一些项改变或者移除了,还添加了一些项。组合所有的更新需要一点点功夫。
使用外键关联持久化修改
在连接的情况下很简单,直接上代码吧:
清单7.10 在连接情况下更新订单
order.ActualShippingDate = DateTime.Now.AddDays(2); var product1 = new Product() { ProductId = 3 }; var detail1 = new OrderDetail() { Product = product1, Quantity = 5, UnitPrice = 3 }; order.OrderDetails.Add(detail1); order.OrderDetails[1].Quantity = 2; ctx.OrderDetails.DeleteObject(order.OrderDetails[2]); ctx.SaveChanges();
这段代码执行四个操作。订单的shipping date被更新了,添加了一个新的detail,修改了一个已存在detail的quantity,删除了一个detail。是不是很容易理解?
断开连接的情况下,有一点点困难。假设在更新逻辑放在外部方法中——它不知道什么已经修改、添加或者移除了。它接收order,然后需要一种方式知道什么被修改了。在分层应用程序中,这是常见的情况。
解决方法也很简单。查询数据库检索order和它的details。然后使用ApplyCurrentValues方法用传入order的值更新来自数据库的order。可是,ApplyCurrentValues只对order有影响,details还是保持不变。要想知道details什么被修改了,就得使用LINQ to Objects。
added状态的order details在传入的order中而不是在从数据库检索的order中。移除的order details在来自数据库的order中,但是还没有被方法接收。在检索自数据库和传入的order中的order details可能已经使用ApplyCurrentValues修改了。通过下图很容易明白:
在匹配的最后,来自数据库的order就会被传入的数据更新并且准备好被持久化。看下面的清单:
清单7.11 在断开连接的情况下更新订单
void UpdateOrder(Order order) { using (var ctx = new OrderITEntities()) { var dbOrder = ctx.Orders.Include("OrderDetails") .First(o => o.OrderId == orderId); var added = order.OrderDetails.Except(order2.OrderDetails); var deleted = order2.OrderDetails.Except(order.OrderDetails); var modified = order2.OrderDetails.Intersect(order.OrderDetails); ctx.Orders.ApplyCurrentValues(order); added.ForEach(d => dbOrder.OrderDetails.Add(d)); deleted.ForEach(d => ctx.OrderDetails.DeleteObject(d)); modified.ForEach(d => ctx.OrderDetails.ApplyCurrentValues(d)); ctx.SaveChanges(); } }
使用独立关联持久化修改
当涉及到独立关联时,修改order和它的details的代码保持不变,不同的是关联customer的方式。
因为没有外键属性,ApplyCurrentValues不改变order和customer之间的关联。改变customer的唯一方式是分配一个表示新的customer的实例给Order的Customer属性。
如果customer实例没有附加到上下文,它会在Added状态中关联到上下文。显然不是你需要什么,因为你不想插入customer而是仅仅修改和order关联的customer。有三种方式这么做:
下面的清单显示了如何使用第三个方法改变customer关联。
清单7.12 使用独立关联修改order的customer
var order = GetOrder();
ctx.Companies.Attach(order.Customer);
dbOrder.Customer = order.Customer;
ctx.SaveChanges();
使用外键属性持久化删除
在连接情况下,检索order,调用DeleteObject,然后调用SaveChanges。
在断开连接的情况下,创建一个填充了ID的order实例,附加它到上下文,调用DeleteObject和SaveChagnes。下面的清单显示了在断开连接的情况下删除关系图有多简单。
清单7.13 断开连接情况下使用外键关联删除一个order
void DeleteOrder(int orderId) { using (var ctx = new OrderITEntities()) { Order order = new Order() { OrderId = orderId }; ctx.Orders.Attach(order); ctx.Orders.DeleteObject(order); ctx.SaveChanges(); } }
这里有一点性能问题需要注意。即使EDM知道在数据库中有级联约束,上下文也会为每个附加到上下文的实体发出DELETE命令。
假设一个订单有30个details。你可能希望为整个Order发出一个delete,但并不是你想的那样。在概念模式中,order和它的details之间的级联约束是指定的;所以,如果它们被附加到上下文,就会标记为deleted并且为它们的每一个对数据库发出一个delete。
当然,如果没有删除级联约束,必须检索所有的details并将让它们标记为deleted。可惜,这个解决方案总是容易出错,因为在检索details和它们的物理删除之间的时间,可能添加了一个新的detail,而上下文并不知道。当从数据库中删除数据时,这会导致一个外键错误,因为当order删除时这个detail没有被删除。
替代解决方案是发起一个删除所有details的自定义数据库命令,然后让上下文对order发出一个单独的delete。
你大概在想,如果删除一个order的同时,其他人又添加了一个detail,那应该有个并发检查。在下一章,我们就讨论它,这里将它忽略。 |
最后,如果使用删除级联,一切都很简单。代码不用关心details,数据库的性能也提高。不使用级联,在删除过程中就需要有更多的控制,但那不以为着需要更多的代码和性能的下降。
关于使用外键属性删除实体就这些。
使用独立关联持久化删除
从概念上来说,使用独立在关系图中关联删除实体和使用外键属性在关系图中删除实体没有区别。改变的是代码,因为删除一个实体,上下文需要附加所有的one-to-one关联实体。如果需要删除一个customer,不需要orders,因为它在many一方,如果要删除一个order,则需要它的customer。
删除一个order需要customer的原因是state manager标记两个实体的关系为Deleted并转换成SQL,为order的DELETE命令的WHERE子句添加了外键列。最后,删除一个order,不是发出DELETE FROM Order WHERE OrderId = 1而是DELETE FROM order WHERE OrderId = 1 AND CustomerId = 2。
在连接情况下,独立关联和外键关联的区别不存在。当检索order时,state manager已经知道了它的customer和它们的关系。在断开连接的情况下,必须和order一起附加customer,这很重要。你需要做的就是创建order,关联相关的customer,附加order到上下文,然后传递它到DeleteObject方法。因为级联约束存在,details也会自动删除。看下面的清单:
清单7.14 使用独立关联删除一个order
void DeleteOrder(int orderId, int customerId) { using (var ctx = new OrderITEntities()) { var order = new Order() { OrderId = orderId }; order.Customer = new Customer() { CompanyId = 1 }; ctx.Orders.Attach(order); ctx.Orders.DeleteObject(order); ctx.SaveChanges(); } }
如果删除级联约束不存在,要为外键属性做同样的考虑。唯一需要指出的是为了删除details,它们关联的products也必须加载(就像order需要customer一样)。不使用删除级联删除order,必须加载整个关系图。
多对多关系就不介绍新东西了。在前面已经了解了所需要的一切知识。为supplier添加一个product,你创建supplier和product的实例,添加product到supplier的产品列表并调用SaveChanges。
使用相反的方式可以得到相同的结果。创建supplier和product,添加supplier到suppliers的产品列表并调用SaveChanges。
当在连接情况下从supplier移除一个product时,从supplier的产品列表移除product并调用SaveChanges。在断开连接的情况下,从数据库里检索数据,将数据和传入的数据比较来识别改变。如你所见,没有什么新东西。
现在我们来看一个新问题。如果你错误的将order关联到一个不存在的customer会发生什么呢?当持久化时出现错误会发生什么?如果需要执行一个自定义命令又该怎么办?这些问题将在下一节给出答案。