7.4 持久化的一些技巧
持久化过程中的很多东西都有可能引起错误,如字符串太长,违反外键约束,不为空的列设置为null,重复键等等。这些都是引发异常的典型原因。让我们看看如何在代码中处理这些异常吧。
7.4.1 处理持久化异常
一般来说,处理异常很简单,在try/catch块中调用SaveChange即可。由EF引起的异常也是如此,但是要捕捉一个特定的异常:UpdateException。UpdateException包含entry的相关信息,其实体持久化引发了错误。
entry由StateEntries属性公开,它是ReadOnlyCollection<ObjectStateEntry>类型的。StateEntries属性是一个列表,因为它为引起异常的实体返回一个EntityEntry,加上所有与之相关的RelationshipEntry实例。你只接收一个实体,因为只要遇到问题,持久化过程就会停止,当前的entry传递给该异常。
重要的信息也存储在InnerException。在里面可以找到由托管提供程序引起的原始SqlExcption。
如果结合来自内部的异常信息和entries里的信息,就可以构建一个日志记录,它对于理解哪里发生了错误非常有用。如果想更进一步,你可以构建一个窗体,给用户足够的信息理解是什么问题以及如何解决它。下面的清单显示了如何获取异常和写入日志。
清单7.15 处理持久化异常
try { ... ctx.SaveChanges(); } catch (UpdateException ex) { Log.WriteError(ex.StateEntries, ex.InnerException); }
处理错误很简单。我们建议将SaveChanges放在一个方法中,每次持久化时调用它。通过这种方式,就不用每次保存数据时都写try/catch块了。
你必须知道关于持久化的下一个特性是如何发送自定义命令给数据库来更新数据。
7.4.2 执行自定义SQL命令
EF可以持久化任何的修改,但是有些情况下使用自定义SQL命令比使用EF简单。让我们看看在OrderIT应用程序中自定义SQL命令是如何起到帮助作用的吧。
在OrderIT中,总有0库存的项目。当添加了新产品或者出售出去时,这个值是不会被更新的。这是因为在第5章中,你将AvailableItems属性映射到Product表的AvailableItems列并设置属性为Computed。也就是说在数据库中更新映射列时,这个属性从没有被使用过。当持久化实体时,computed列的值直接从数据库中查询放入到属性中。
从本质上来说,此方法消除了上下文通过将更新委托给数据库或者像存储过程一样的自定义SQL命令来更新可用项。此方法有必要正确地计算可用项的值。
正确计算可用项,需要修改前面写的代码,加入以下操作:
- 当创建product时,让上下文在数据库中创建行,然后发出一个SQL命令更新AvailableItmes列。
- 当创建或更新订单时,如果detail是新的,就发出一个SQL命令从库存数量中减去出售的数量;如果detail被移除,就发出一个SQL命令,往库存数量中加入添加出售的数量;如果detail被吸怪了,就发出一个SQL命令,然后从库存量中减去旧的数量,然后加入新的数量。
为什么这样做?为什么不让EF持久化列?原因有二:并发性和简洁性。
- 并发性—假设两个人同时创建订单。在同一时间它们都读取product的数据,然后第一个人更新了数据;第二个人随后将更新过期数据。并发检查可以是一个解决方案,但会浪费用户的时间。用户不应该因为库存量发生了改变就重新插入订单。
- 简洁性—更新一个product需要该product被附加,即使使用外键关联也是如此。另外,必须更新AvailableItems属性和所有的实体状态导致更多复杂的代码。手动更新更直接,减少了复杂性。
这种方式执行任务相当简单。调用SaveChanges之前,取得所有处于Modified状态的details,并且添加quantity到AvailableItems。SaveChanges之后,取得所有Added和Modified状态的实体,从AvailableItems减去quantity。最后,每个处于Deleted状态的detail实体,添加quantity到AvailableItems。
执行自定义SQL命令,使用ObjectContext类的ExecuteStoreCommand方法。这个方法和在第四章中见到的ExecuteStoreQuery<T>方法有相同的功能。
清单7.16 执行自定义SQL命令更新数据
ctx.ExecuteStoreCommand("Update Product set AvailableItems = AvailableItems - 3 where productid = 1");
在第四章学过,在程序代码中嵌入SQL并不是好主意,因为需要将你的程序绑定到指定的数据库。这在大多数情况下并不是问题,但存储过程更可取。在第10章会学习如何用存储过程替换前面的代码。
这一章又结束了,下一章学习处理并发性和事务。