摘要

这个问题最早是由zq同学经过一个非常艰苦的过程发现的,而后来我、bojian、mtt同学一起在另外一个代码库里又遭遇了一回,还好在zq经验的指引下我们第二次很快发现了问题。在项目里,这也算有点黑历史的问题了。
这种defact是因为在.net体系下,关系模型映射到对象模型过程中,对于数据库datetime列空值的不同处理导致的defect。在production环境上往往以伪随机的面貌出现。
如:存在entity b和table b,并且entity b和table b存在映射关系。
entity b中的属性updateDate为datetime类型,而table b对应的列updateDate为可空的datetime类型时。
如果在程序运行过程中

  • nhibernate在table b读取到updateDate为NULL的一行数据,
  • 用这行数据实例化了一个entity b的实例,
  • 在程序运行过程中对这个实例的updateDate属性没有再赋予新值,

那么当执行session.flush时,nhibernate会抛出“System.Data.SqlTypes.SqlTypeException: SqlDateTime overflow. Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM”这样的错误。
而当从table b读取到updateDate不为NULL的一行数据时,不会发生任何错误。
解决方案:
将entity b的属性updateDate的类型从datetime改为datetime?(Nullable)就可以解决这个问题了。

TL;DR
以下说明了我们在production上遇到这个问题已发的issue时,如何debug到了这个问题; nhibernate的什么处理导致了这个问题。

问题背景

如:项目里存在这样的两个领域对象,且这两个领域对象都mapping到了数据库对应的表上
领域对象:

Entity A
id int
name string
end Entity

Entity B
id int
date datetime
end Entity

表格:

table A
id int
name varchar(4000)
end table

table B
id int 
updatetime datetime nullable
end table

业务流程如下:

open session
session.load(B)
read B
session.load(A)
updae and save A
session.flush
close session

当session.flush时,会“随机”报 System.Data.SqlTypes.SqlTypeException: SqlDateTime overflow. Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM。

问题分析

第一次分析

通过日志,我们只能得到结论:这里存在着更新了某个类型为时间的属性的值,而这种值是不符合sql date time要求的。从经验上看,可以粗略判断应该是有地方设置这个属性值为DateTime.MinValue。
通过现有的log,我们已经不能获得再多的知识了。我们开始了代码走查,重点关注相关业务流程中涉及DateTime的。
但是遍历了整个代码,没有找到类似的赋值。而且在从业务上来看,也没有更新带有时间属性的Domain对象的需求!!
nhibernate到底搞什么鬼!!!!

再次抓取日志

分析代码这条路走不通了,并且因为报的是sqltype类型错误,所以nhibernate也没有pass sql语句到数据库,就是说无法通过sql profile来调试。
幸好,我们还可以通过nhibernate提供的Interceptor机制来监控session.flush过程中更新了哪些entity的什么属性,老值是什么样,新值是什么样。具体可以参见http://alexander.lds.lg.ua/2010/04/nhibernate-–-part-2/这篇博客中的描述。
通过监控时间类型,我们找到了一批有问题的实体(整个监控过程....惨绝人寰)。这些实体竟然都是前文中提到的Entity B,而且都是老值为null值,新值为DateTime的最小值.......
这时候,聪明的同学可能已经找到了答案:对,就是关系模型映射到对象模型时,对于可空列的映射是错误的。
为了严谨,我们分别构建了两行数据:这两行数据的区别在于一行的date列值为null,一行的date列值不为null。
如前所述的执行过程,在load到date列值为null的数据时,在session.flush时会抛出异常;而load到的date列不为null的那一行数据时,则不抛出异常。

最终结论

这是因为Entity B的date属性的类型是datetime,这种类型在.net里属于struct,不能被赋予null值。
而Table B的date列确实可空的。
当nhibernate从table b中load到一行date列值为null的数据后,因为.net不允许将null赋值到Entity B实例的date属性上,这个时候nhibernate就很天才的没有给date属性赋值,即此时的date属性的值为datetime的默认值...
而当session.flush的时候,nhibernate会发现entity b因为date属性的新值是DateTime的默认值而是不是null,就将B作为了一个dirty的entity,试图flush到数据库,在生成sql语句的过程中,挂了!

最佳实践

对于这种类型不匹配,在nhibernate中已经有了处理,就是利用.net提供的nullable类型。
新的entity B,如下:

Entity B
id int
date datetime?(Nullable<datetime>)
end Entity

在这里,Entity B的date属性不再是datetime而是datetime?(Nullable),即经过包装的datetime类型。经过包装的datetime?,是可以被赋予空值的,因此nhibernate在load数据过程中就不需要自作聪明的绕过date属性,而是把null赋给date属性。
当session.flush时,因为entity b date属性的原值和现值都是null,也不会被flush到数据库,问题解决。