当为员工创建帐号并分配相应的权限后该帐号即可登录系统并进行相应的操作。当员工与系统进行交互操作时系统会把员工Id、操作时间、操作IP、操作内容等信息记录到操作日志中以便随时审计。这样从Domain的角度讲操作日志对象与员工对象之间存在many-to-one的引用关系从Database的角度讲操作日志表的员工Id列是外键其引用员工表的主键Id列。如果遵照数据库设计第二范式2NF那么操作日志表包含员工Id列外键但不包含员工姓名、员工帐号等冗余字段。这样当查询操作日志获取操作信息的同时要想获取员工姓名、员工帐号等数据需要对操作日志表与员工表在员工Id列上进行一个连接inner join或outer join操作。这是一个大家比较熟悉的应用连接操作的场景但在实际项目中可能会变得稍微复杂一点比如对员工数据的删除。大家都知道如果操作日志与员工之间建立了关联外键引用关系那么在未删除该员工所有的操作日志数据之前删除该员工数据会发生外键冲突 foreign key violate因为在删除员工数据时数据库会自动检测并确保外键引用的完整性referential integrity。解决方法有四个1在物理删除员工数据之前先删除该员工的所有操作日志数据然后再删除该员工数据或者在数据库中设置级联删除在物理删除员工数据时级联删除该员工的所有操作日志数据。尽管这样可以避免外键冲突但是这个方法显然是不可行的因为删除操作日志数据也就意味着破坏了审计功能。2不物理删除员工数据而是进行“软删除”soft-delete也就是说为员工数据增加一个“是否删除”的标记列当“删除”员工时设置此标记值。由于没有真正删除员工数据也就避免了外键冲突问题。这可能是最常见的做法但是soft-delete也不是没有问题尤其是使用NHibernate进行面向DDDDomain Model Development的开发。大家可以看看这几篇文章Avoid Soft Deletes文章后面的评论也精彩、Soft Deletes aren’t Append Only model、Don’t Delete – Just Don’t、Soft-deletes are bad, mkay?。3 操作日志与员工之间不建立关联外键引用关系在数据库中仍可通过对操作日志表和员工表在员工Id列上进行连接在一条查询语句中获取两张表的数据。由于没有建立关联外键引用关系所以员工数据的物理删除不会 引起外键冲突问题。但是这个方法会引发别的问题当员工数据被物理删除后该员工的操作日志数据与该员工进行连接查询时如果进行的是inner join那么连接查询结果就为空如果进行的是left outer join那么在连接查询结果数据行中关于该员工的字段信息员工姓名、员工帐号都为null具体可参见outer join的相关资料。4 操作日志与员工之间不建立关联外键引用关系并且在操作日志表中除包含员工Id字段外还包含员工姓名、员工帐号等冗余字段这样直接查询操作日志表即可获得员工姓名、员工帐号等信息无需再与员工表进行任何连接操作因此还可提高查询性能同时员工数据的删除也不影响操作日志表但是此方法违反了数据库设计的2NF。在实际开发中为了性能的提高、为了实现的简单性有时出现类似这样的反模式是可以接受的应该不是问题问题是有冗余就一定会存在数据一致性问题比如当修改员工姓名后操作日志表中该员工的操作日志数据的员工姓名冗余字段是否也需要同步更新呢在这种情况下对操作日志等历史数据进行同步更新往往没有必要因为修改员工姓名这类操作不会经常发生而且不同步更新正好还可以保留操作员工当时时刻的姓名。由上面的分析可见为实现员工操作的审计功能、确保员工数据的“删除”不引发外键冲突方法2与方法4应该是较为可行的方案。当然有的朋友会认为员工数据不该“删除”而应该通过设立一个表示禁用、启用的状态字段来解决当然这与方法2在本质上是类似的。说了这么多关于数据库设计的内容不少朋友会怀疑本文是否偏离了该说的主题NHibernate接下来就进入主题我们选择方法3做为本文说明的实例。方法3的关键是在操作日志与员工之间不建立关联外键引用关系然后在员工Id列上进行连接操作。那么在NHibernate中如何对没有建立关联关系的实体进行连接操作呢答案是使用theta-style join。本文首先介绍theta-style join与常见join的区别然后通过此实例具体阐述在NHibernate中对无关联实体进行theta-style join的实现。二、实例场景有员工Employee和操作日志OperationLog两张数据表为说明问题这两张表之间不建立外键引用关系。创建数据表的SQL如下Code接着往数据表中插入测试数据如下图所示三、常见join与theta-style join的差别我们对没有建立外键引用关系的Employee表和OperationLog表分别进行常见join和theta-style join的连接操作两者SQL写法的区别如下1常见join的SQL写法在inner join中指定要连接的表在on中指定连接条件。SQL92标准select OperationLog.Id, OperationLog.OperationDateTime, Employee.Namefrom OperationLog inner join Employeeon OperationLog.EmployeeIdEmployee.Id2theta-style join的SQL写法在from中指定要连接的表在where中指定连接条件。SQL89标准select OperationLog.Id, OperationLog.OperationDateTime, Employee.Namefrom OperationLog, Employeewhere OperationLog.EmployeeIdEmployee.Id在关系数据库中尽管没有为Employee表和OperationLog表建立外键引用关系但是仍然可以用SQL在任意列只要类型兼容上进行常见join或theta-style join的操作当然是否在外键列上进行连接操作会影响到数据库查询优化器对连接操作算法的选择从而影响连接查询的性能而且对于上面这个例子这两个连接操作逻辑上是等价的查询结果是相同的连接查询结果如下图所示但在NHibernate中对无关联实体进行连接操作只能使用theta-style join而无法使用常见join而且仅HQL支持theta-style joinCriteria并不支持theta-style join。接下去通过实例进行验证四、使用HQL的theta-style join实现无关联实体的连接先分别建立Employee与OperationLog实体类注意两个实体类没有建立关联关系Employee.csnamespace NHibernatePractice3.Domain{public class Employee{public virtual int Id { get; set; }public virtual String Name { get; set; }}}OperationLog.cs:Code接着创建mapping文件注意在mapping文件中不建立关联关系Employee.hbm.xml:CodeOperationLog.hbm.xml:Code接下去写测试方法测试theta-style join的实现