NHibernate中关于Inverse的理解和使用
在项目中NHibernate进行ORMapping,操作数据库变得非常简单,但是NHibernate中有很多特性不是很容易理解,比如Inverse这个功能就是其中的一个。
在使用NHibernate进行数据库操作的时候,比如数据插入的时候,经常用到级联功能,比如最常见的就是一个订单对应多个明细行,在保存订单时只需要Save订单对象即可,订单下的所有明细行会级联保存。在对象模型层面,Order对象中有个属性IList<OrderItem> Items,对应其中的订单明细OrderItem。对于OrderItem对象,其中可以没有Order对象的引用,如果有Order对象的引用,那么就是双向关联Bidirectional!
对于Bidirectional的情况,那么在保存数据到数据库时就会涉及到一个问题,如果两边的数据不一致,也就是mismatch,到底是以Order中的Items为准还是以OrderItem中的Order为准?NHibernate Cookbook中是这样说的:
To work around this mismatch, NHibernate ignores one side of the bidirectional relationship. The foreign key in the database is populated based on either the OrderItems reference to the Order or the Orders collection of OrderItems, but not both. We determine which end of the relationship controls the foreign key using the inverse attribute on the collection. By default, the Order controls the foreign key. Saving a new Order with one OrderItem will result in the following three SQL statements:
INSERT INTO "Order" (Id) VALUES (@p0) INSERT INTO OrderItem (Id) VALUES (@p0) UPDATE OrderItem SET OrderId = @p0 WHERE Id = @p1
When we specify inverse="true", the OrderItem controls the foreign key. This is preferable because it eliminates the extra UPDATE statement, resulting in the following two SQL statements:
INSERT INTO "Order" (Id) VALUES (@p0) INSERT INTO OrderItem (OrderId, Id) VALUES (@p0, @p1)
大体意思就是,NHibernate默认使用Order的属性作为有效的关联,换句话说,只需要把OrderItem一个个的加入到Order的Items集合即可,最终结果不需要关心OrderItem中引用的Order到底是什么或者为空。如果在Mapping配置Order的Item时设置inverse="true",那么NHibernate就会使用OrderItem的Order引用作为关联。
SQL语句上可以看到明细的区别,在默认Inverse为false的情况下,在保存OrderItem时,其数据库的字段OrderId是设为null,然后再将Order的Id重新Update到OrderItem中。
【注意:这里是说最终结果,而不是中间结果,在Insert OrderItem的时候,其OrderId为该对象对应的Order对象的Id,如果该Order对象未保存,则OrderId为null,如果是已保存的,则是该Order的Id,然后接下来会更新该OrderId。】
接下来举一个具体的例子,部门和员工,一对多关系,部门D1,D2,员工U1和U2,D1的Users里面有U1和U2,U1对象引用D1,U2对象引用D2。
Department d1=new Department(){Name = "D1"}; Department d2=new Department(){Name = "D2"}; User u1=new User(){Name = "U1",Department = d1}; User u2=new User(){Name = "U2",Department = d2}; d1.Users=new List<User>(){u1,u2};
默认不设置Inverse的情况下如果先保存d1,后保存d2,会生成如下的SQL:
NHibernate: INSERT INTO DEPARTMENT (NAME, DEPARTMENT_ID) VALUES (@p0, @p1);@p0 = 'D1' [Type: String (0)], @p1 = 100000000100000 [Type: Int64 (0)] NHibernate: INSERT INTO USER (NAME, DEPARTMENT_ID, USER_ID) VALUES (@p0, @p1, @p2);@p0 = 'U1' [Type: String (0)], @p1 = 100000000100000 [Type: Int64 (0)], @p2 = 100000000100000 [Type: Int64 (0)] NHibernate: INSERT INTO USER (NAME, DEPARTMENT_ID, USER_ID) VALUES (@p0, @p1, @p2);@p0 = 'U2' [Type: String (0)], @p1 = NULL [Type: Int64 (0)], @p2 = 100000000100001 [Type: Int64 (0)] NHibernate: INSERT INTO DEPARTMENT (NAME, DEPARTMENT_ID) VALUES (@p0, @p1);@p0 = 'D2' [Type: String (0)], @p1 = 100000000100001 [Type: Int64 (0)] NHibernate: UPDATE USER SET NAME = @p0, DEPARTMENT_ID = @p1 WHERE USER_ID = @p2;@p0 = 'U2' [Type: String (0)], @p1 = 100000000100001 [Type: Int64 (0)], @p2 = 100000000100001 [Type: Int64 (0)] NHibernate: UPDATE USER SET DEPARTMENT_ID = @p0 WHERE USER_ID = @p1;@p0 = 100000000100000 [Type: Int64 (0)], @p1 = 100000000100000 [Type: Int64 (0)] NHibernate: UPDATE USER SET DEPARTMENT_ID = @p0 WHERE USER_ID = @p1;@p0 = 100000000100000 [Type: Int64 (0)], @p1 = 100000000100001 [Type: Int64 (0)]
仔细分析这些SQL语句,就会发现在insert保存U1时,其DepartmentId是有值的,而Insert保存U2时,其DepartmentId是null,这是因为D2现在还没有保存到数据库,没有Id,所以插入Null,接下来是保存D2,在保存了D2后有了Id,那么就需要更新U2的DepartmentId,让其等于D2的Id。以上都是插入过程,接下来还要进行外键更新操作,保证数据库中的外键与对象中Department中设置的Users保持一致,所以Update每个User表即可。
如果是改为Inverse=True,那么然后保存d1和d2,那么对应的SQL是:
NHibernate: INSERT INTO DEPARTMENT (NAME, DEPARTMENT_ID) VALUES (@p0, @p1);@p0 = 'D1' [Type: String (0)], @p1 = 100000000100000 [Type: Int64 (0)] NHibernate: INSERT INTO USER (NAME, DEPARTMENT_ID, USER_ID) VALUES (@p0, @p1, @p2);@p0 = 'U1' [Type: String (0)], @p1 = 100000000100000 [Type: Int64 (0)], @p2 = 100000000100000 [Type: Int64 (0)] NHibernate: INSERT INTO USER (NAME, DEPARTMENT_ID, USER_ID) VALUES (@p0, @p1, @p2);@p0 = 'U2' [Type: String (0)], @p1 = NULL [Type: Int64 (0)], @p2 = 100000000100001 [Type: Int64 (0)] NHibernate: INSERT INTO DEPARTMENT (NAME, DEPARTMENT_ID) VALUES (@p0, @p1);@p0 = 'D2' [Type: String (0)], @p1 = 100000000100001 [Type: Int64 (0)] NHibernate: UPDATE USER SET NAME = @p0, DEPARTMENT_ID = @p1 WHERE USER_ID = @p2;@p0 = 'U2' [Type: String (0)], @p1 = 100000000100001 [Type: Int64 (0)], @p2 = 100000000100001 [Type: Int64 (0)]
可以看出,最大的不同是没有了最后两句更新外键的SQL。如果我们再调整下保存的顺序,先保存D2,然后再保存D1,那么对应的SQL是:
NHibernate: INSERT INTO DEPARTMENT (NAME, DEPARTMENT_ID) VALUES (@p0, @p1);@p0 = 'D2' [Type: String (0)], @p1 = 100000000100000 [Type: Int64 (0)] NHibernate: INSERT INTO DEPARTMENT (NAME, DEPARTMENT_ID) VALUES (@p0, @p1);@p0 = 'D1' [Type: String (0)], @p1 = 100000000100001 [Type: Int64 (0)] NHibernate: INSERT INTO USER (NAME, DEPARTMENT_ID, USER_ID) VALUES (@p0, @p1, @p2);@p0 = 'U1' [Type: String (0)], @p1 = 100000000100001 [Type: Int64 (0)], @p2 = 100000000100000 [Type: Int64 (0)] NHibernate: INSERT INTO USER (NAME, DEPARTMENT_ID, USER_ID) VALUES (@p0, @p1, @p2);@p0 = 'U2' [Type: String (0)], @p1 = 100000000100000 [Type: Int64 (0)], @p2 = 100000000100001 [Type: Int64 (0)]
显然第一种SQL语句进行了外键的update操作,没有第二三次的效率高,而且,必须要设置数据库中OrderItem的OrderId允许为空。从数据库模型来说,这个不合理啊!
所以一般建议在Mapping时设置Inverse为True。对应的,在Code中也需要设置OrderItem对Order的引用。
Inverse更大的用处是在ManyToMany的时候。如果两边Inverse=False的情况下,ManyToMany是任意一边设置集合并保存就有效,如果两边都设置的话,会保存多次。比如有员工E1和E2,奖品A1和A2,其是多对多关系,如果要设置E1员工获得A1和A2奖,那么需要设置各自的集合:
1: Emp e1=new Emp(){Name = "E1"};
2: Emp e2 = new Emp() { Name = "E2" };
3: Award a1=new Award(){Name = "A1"};
4: Award a2 = new Award() { Name = "A2" };
5: e1.Awards=new List<Award>(){a1,a2};
6: a1.Emps=new List<Emp>(){e1};
7: a2.Emps = new List<Emp>() { e1 };
从DomainModel来说,这样设置是对的,但是生成SQL却有问题:
1: NHibernate: INSERT INTO EMP (NAME, EMP_ID) VALUES (@p0, @p1);@p0 = 'E1' [Type: String (0)], @p1 = 1000000001 [Type: Int64 (0)]
2: NHibernate: INSERT INTO AWARD (NAME, AWARD_ID) VALUES (@p0, @p1);@p0 = 'A1' [Type: String (0)], @p1 = 1000000001 [Type: Int64 (0)]
3: NHibernate: INSERT INTO AWARD (NAME, AWARD_ID) VALUES (@p0, @p1);@p0 = 'A2' [Type: String (0)], @p1 = 1000000002 [Type: Int64 (0)]
4: NHibernate: INSERT INTO EMP (NAME, EMP_ID) VALUES (@p0, @p1);@p0 = 'E2' [Type: String (0)], @p1 = 1000000002 [Type: Int64 (0)]
5: NHibernate: INSERT INTO AWARD_EMP (EMP_ID, AWARD_ID) VALUES (@p0, @p1);@p0 = 1000000001 [Type: Int64 (0)], @p1 = 1000000001 [Type: Int64 (0)]
6: NHibernate: INSERT INTO AWARD_EMP (EMP_ID, AWARD_ID) VALUES (@p0, @p1);@p0 = 1000000001 [Type: Int64 (0)], @p1 = 1000000002 [Type: Int64 (0)]
7: NHibernate: INSERT INTO AWARD_EMP (AWARD_ID, EMP_ID) VALUES (@p0, @p1);@p0 = 1000000001 [Type: Int64 (0)], @p1 = 1000000001 [Type: Int64 (0)]
8: NHibernate: INSERT INTO AWARD_EMP (AWARD_ID, EMP_ID) VALUES (@p0, @p1);@p0 = 1000000002 [Type: Int64 (0)], @p1 = 1000000001 [Type: Int64 (0)]
明明应该是往中间表插入2条记录的,但是这样5-8行却变成了插入4条记录。如果中间表设置了联合主键,那么必然会报错,插入失败。
这个时候可以在Award端设置Inverse=True,Emp端设置Inverse=False,表示其多对多关系不在Award方维护,只在Emp端维护:
public class AwardMapping : IAutoMappingOverride<Award> { public void Override(AutoMapping<Award> mapping) { mapping.HasManyToMany(a => a.Emps).Inverse(); } } public class EmpMapping : IAutoMappingOverride<Emp> { public void Override(AutoMapping<Emp> mapping) { mapping.HasManyToMany(a => a.Awards).Not.Inverse(); } }
这样设置了Mapping后,就可以生成正确的SQL语句,当然如果把C#代码中的6行和7行去掉,结果也是正确的,因为现在系统只认Emp中的Awards集合了。但是如果删除第5行,保留6-7行则不行。
总结:
Inverse用于设置双向关联时Nhibernate在设置外键时依赖的对象,默认Inverse=False,一对多时表示依赖一端的集合,如果为True表示依赖多段对象中对一端对象的引用。
多对多时不能让两端的Inverse为False,这样会造成数据的重复插入;必须设置一端为False,一端为True。