实体关联错误信息的实现

业务场景

为提高ERP软件的易用性,有时候希望即使某张单据包含错误的数据,我们仍然允许操作员保存此单据,只要不是数据库不允许的行为即可。例如,采购订单明细的数量为0,但是操作员已经录入很多明细了,他希望能够立即保存单据,因为他要去吃饭。这个功能在工作流表单系统中经常出现,叫做暂存。当这个操作员吃饭回来后,重新打开这个单据,之前校验的错误信息又希望仍然可以重现。
这里可能有些人会说,“数量不能为0“可以留到入账(有些软件叫审核)前检查,但是我认为,单据在编辑时也能够指出错误,对于客户会比较友好些。
为进一步提高易用性,我们更希望发生错误时,错误信息应该显示在相关字段的控件上。
另外,你是否发现,当我入账时,系统报告了一堆的错误,当我关闭错误对话框后,我又回忆不起来有那几个错误需要修改?

处理

基本概念

首先你需要理解,校验行为一般有两个时机点,一个是在操作员编辑单据时实时校验出来的,错误信息被存放在实体上(如果你不清楚为什么放在实体上,可以看看System.ComponentModel.IDataErrorInfo)。还有一个是当用户保存数据或入账时,这个时机点进行的校验一般代码放在服务端处理。
其次,你需要理解这里所讲的校验均是“针对数据,不针对人”,就是说,校验出的错误描述的应该是数据违反了商业规则,而不是这个操作员违反了什么规则。举个例子,“您(指当前操作员)没有入账100万以上单据的权限”,这个错误就不能记录到实体上,如果你记录了,意味着另外一个操作员打开此单据后看到此错误,他会很迷惑。

将错误消息记录到实体上

当实时校验出新的异常后,首先要确定(通过诸如配置或者Attribute)此异常存放在哪个实体上的哪个属性上,然后调用实体上提供的
SetError(string propertyName,string key,string message);
注意:此方法需要你自己为实体设计,默认的DataSet模型下,其DataRow虽然提供了Set方法,但是未提供Key的概念,那么我们来看看为什么需要Key。
在销售订单上的客户属性上,有两个校验,一个是“客户不能是此订单销售员禁止销售对象”,一个是“客户不能为停用”。这个时候,操作员录入了两个校验都不能通过的数据后,第二个校验器就会写入错误信息到客户属性上,这时,如果没有Key的概念,前面校验的错误信息将被冲掉。如果有Key的概念,我们为每个校验定义不同的Key,那么就不会冲掉之前的校验。
SetError内部代码看起来像这个样子(抱歉,家里的机器没有安装VS,所以没有办法验证代码的准确性,仅表达意思)。
SetError 设置错误
当然,同样的,如果在实时校验发现某个错误已经消失了,需要调用RemoveError方法。
RemoveError
应该说,上面的方法仅是示例,你实际的代码还要考虑错误的类型,错误的类型包括:警告、错误和严重错误。

由于我们将错误信息保存到实体上,所以错误信息会随实体数据一并传递到服务器端的保存方法上。
备注:如果你使用诸如WebService之类的网络协议时,你需要小心,你的序列化规范中很可能仅传递了数据而丢失了错误信息。
回到主题,服务器的保存方法,需要再次调用保存这个时机点的校验工作,此次校验有可能再次新增出新的错误信息。同样的,我们将错误信息保存到实体上。

保存和还原错误信息

当保存方法完成校验后,需要检查实体上的所有错误,如果包含“严重错误”,这里严重错误一般指此数据违反了数据库规则。那么保存方法将抛出一个异常,但是注意的是,抛出的异常将包含最后的实体。为什么抛出的异常要包含最后结果的实体呢?很简单,客户端需要拦截此异常,然后获取到包含最后校验结果的实体,重新展现到界面上,这样界面就显示了服务器端最后校验的结果。
那么,如果没有任何“严重错误”,那么保存方法将继续,有两种思路保存异常信息。
一、提取实体上的错误信息,然后保存到另外一张表中。这种方法看起来非常优雅,因为他没有破坏实体的表结构,而且你可以但是这种方法在使用对象化的实体时,有很大的麻烦,因为对象层次很可能很深(例如明细的明细),那么此种方法就需要使用复杂的算法,以便在读取实体时又能够还原到实体上。还有一个问题是,此方法增加了数据库的额外SQL以便保存错误。
二、第二种思路是在实体上包含EntityErrors字段,保存方法将每个实体的错误信息转化为XML形式,然后赋值到此实体的EntityErrors字段。这种方法的好处是ORM把EntityErrors作为普通的字段,没有增加额外的SQL,也没有复杂的还原机制。当ORM读取回数据后,Read方法再讲XML形势的错误信息再还原到实体内部结构。这种方法的坏处是破坏了实体的结构,但是我觉得还是值得的。
刚才两种方法其实都提到了,在Read方法中,还需要将数据库中存储的错误信息还原到实体上,这样才能达到这样的效果:用户编辑和保存时的错误信息在再次打开单据时,仍然可以展现给操作员。
另外一个需要注意的地方是,Save方法也需要返回值,因为当校验出新的错误,但是没有“严重错误”时,保存方法将成功完成,如果没有将最后校验后的实体重新传回客户端,那么客户端很可能在保存后没有显示出由Save方法校验出来的新错误。

关于嵌套事务

在入账方法中,事务的处理有些古怪,因为通常我们理解一个方法,如果失败了,事务就应该回滚。看起来像这个样子:

错误处理事务的入账
但是在这个业务中,入账方法的前半部分发生校验行为,到校验完成发现有错误后,程序一定是需要抛出异常来表明入账失败,但是这个时候你会发现校验出的错误信息即时你保存到数据库中了,但是因为发生了回滚,所以入账发现的错误并没有记录在单据上。
正确的方法是把这个方法拆解成两个事物。
正确处理事务的入账
将校验和入账流程拆分出来会有个小问题,可能校验程序检测到客户信用额度是充足的,但是当入账时,客户信用额度已经不足了(因为不是在一个事物中),解决此类问题的最好方法是入账程序在Update这样的语句中包含Where:
 Update 客户信用表 Set 余额=余额-使用量 Where 客户编号=@CustomerId and 余额-使用量量>=0这样的限制条件,来再次校验会更加安全。
此设计你可能需要知道另外一种情况,例如有段自动程序,根据某个向导的结果,自动创建一个单据,然后填充数据,然后保存,再然后自动入账。整个过程应该是全程事务,而这段入账程序也作为嵌套事务,事实上,通过分析,上面拆分事务的入账程序在这种场景下是没有问题的。
需要注意的是,Save方法仍然需要整个方法是一个事物,因为如果你先把错误信息保存了,但是单据却没有成功保存,这个时候另外的操作员打开此单据时,看见的错误和数据就“文不对题”了。

总结

这个业务给程序设计带来了很大的挑战,但是通过努力我们是可以做到的,这种努力是值得的,大大提高了软件的可用性。
原文地址:https://www.cnblogs.com/tansm/p/1568766.html