NHibernate中使用ClassMap实现基于实体唯一信息的级联持久化问题求助
兄弟,我太懂你现在的糟心处境了——想让NHibernate在保存Class的时候,自动识别数据库里已经存在的Classroom和Student(靠它们的业务唯一属性,不是数据库生成的ID),直接复用而不是重复插入,但试了好几种方法都踩坑了对吧?我之前也碰到过几乎一模一样的需求,给你梳理几个可行的解决方案:
方案一:利用NHibernate的NaturalId + Merge方法(最推荐)
NHibernate本身就支持**自然主键(NaturalId)**的配置,用来标记实体的业务唯一属性,配合Merge()方法就能自动实现“存在则复用,不存在则新建”的逻辑。
首先修改你的映射类,给Classroom和Student配置NaturalId:
class ClassroomMap: ClassMap<Classroom> { public ClassroomMap() { Id(x => x.ID).GeneratedBy.Native(); // 配置业务唯一键:FloorNumber + RoomNumber NaturalId() .Property(x => x.FloorNumber) .Property(x => x.RoomNumber); Map(x => x.FloorNumber); Map(x => x.RoomNumber); } } class StudentMap: ClassMap<Student> { public StudentMap() { Id(x => x.ID).GeneratedBy.Native(); // 配置业务唯一键:Name + Homeroom(这里Homeroom已经是带NaturalId的实体) NaturalId() .Property(x => x.Name) .Reference(x => x.Homeroom); Map(x => x.Name); References(x => x.Homeroom).Cascade.SaveUpdate(); } }
然后保存Class的时候,别用Save()或SaveOrUpdate(),改用Merge():
session.Merge(myClass); session.Flush();
Merge()会自动根据NaturalId去数据库查询对应实体:
- 如果存在,就把你传入的 transient 实体的属性合并到持久化实例上,然后关联到
Class里; - 如果不存在,就新建并持久化这个实体,再关联。
方案二:改进你的GetOrCreate方法,确保实例被会话托管
你之前的GetOrCreate逻辑没问题,但报错大概率是因为从数据库查询到的实例不在当前NHibernate会话的托管范围内,导致NHibernate仍把它当成transient实体。
修改你的GetOrCreate和保存逻辑,确保用当前会话查询,并且替换Class里的关联实例:
// 处理Classroom Classroom GetOrCreateClassroom(ISession session, Classroom newClassroom) { var extant = session.Query<Classroom>() .Where(c => c.FloorNumber == newClassroom.FloorNumber && c.RoomNumber == newClassroom.RoomNumber) .FirstOrDefault(); if (extant != null) return extant; session.Save(newClassroom); // 这里可以直接返回newClassroom,因为Save后它已经被会话托管了 return newClassroom; } // 处理Student Student GetOrCreateStudent(ISession session, Student newStudent) { // 先确保Student的Homeroom是持久态的 newStudent.Homeroom = GetOrCreateClassroom(session, newStudent.Homeroom); var extant = session.Query<Student>() .Where(s => s.Name == newStudent.Name && s.Homeroom.ID == newStudent.Homeroom.ID) .FirstOrDefault(); if (extant != null) return extant; session.Save(newStudent); return newStudent; } // 最终保存逻辑 using(var session = SessionFactory.OpenSession()) using(var tx = session.BeginTransaction()) { // 先替换Class里的Classroom为持久化实例 myClass.Classroom = GetOrCreateClassroom(session, myClass.Classroom); // 再替换每个ClassStudent里的Student为持久化实例 foreach(var cs in myClass.Students) { cs.Student = GetOrCreateStudent(session, cs.Student); } session.SaveOrUpdate(myClass); tx.Commit(); }
这样所有关联的实体都是当前会话托管的持久态实例,NHibernate就不会再报“引用transient实体”的错误了。
方案三:自定义事件监听器(适合全局统一处理)
如果不想在业务代码里写重复的GetOrCreate逻辑,可以自定义NHibernate的SaveOrUpdateEventListener,在实体保存前自动检查并替换为已有的实例:
public class UniqueEntitySaveListener : DefaultSaveOrUpdateEventListener { protected override object PerformSaveOrUpdate(SaveOrUpdateEvent @event) { var entity = @event.Entity; // 处理Classroom if(entity is Classroom classroom) { var existing = @event.Session.Query<Classroom>() .Where(c => c.FloorNumber == classroom.FloorNumber && c.RoomNumber == classroom.RoomNumber) .FirstOrDefault(); if(existing != null) { @event.Entity = existing; return existing; } } // 处理Student else if(entity is Student student) { // 先确保Student的Homeroom被处理 var room = @event.Session.Query<Classroom>() .Where(c => c.FloorNumber == student.Homeroom.FloorNumber && c.RoomNumber == student.Homeroom.RoomNumber) .FirstOrDefault() ?? student.Homeroom; student.Homeroom = room; var existing = @event.Session.Query<Student>() .Where(s => s.Name == student.Name && s.Homeroom.ID == student.Homeroom.ID) .FirstOrDefault(); if(existing != null) { @event.Entity = existing; return existing; } } return base.PerformSaveOrUpdate(@event); } }
然后在NHibernate配置里注册这个监听器:
var config = new Configuration(); config.SetListener(ListenerType.SaveUpdate, new UniqueEntitySaveListener()); // 其他配置... var sessionFactory = config.BuildSessionFactory();
之后你正常用SaveOrUpdate()保存Class就可以了,监听器会自动帮你处理重复实体的复用逻辑。
顺便说下你之前踩坑的原因
- 直接用
Cascade.SaveUpdate():NHibernate只会级联保存/更新,但不会去检查业务唯一性,所以必然重复插入; - 加
UniqueKey:这只是让数据库层面报错阻止重复数据,但不会让NHibernate自动复用已有的实例; - 最初的GetOrCreate:查询到的实例可能不在当前会话中,NHibernate无法识别它是持久态,所以报错。
备注:内容来源于stack exchange,提问作者GalaxyGuy

