如何在(Fluent) NHibernate中将枚举映射为整数外键?
需求:将Communication实体中类型为CommunicationType枚举的Type字段,映射为指向CommunicationType表的整数外键。该枚举在数据库中对应整数字段,同时关联存储枚举有效值的表用于报表统计。
相关代码如下:
public enum CommunicationType { General = 1, ServicesContactEnquiry = 2, ChangePassword = 3, OrderDetails = 4, } // 数据库中用于报表统计的枚举对应表实体 public class CommunicationTypeRecord { public virtual int ID { get; set; } public virtual string Name { get; set; } public virtual string Description { get; set; } } public class Communication { public virtual CommunicationType Type { get; set; } // 其他字段省略 } public class CommunicationMap : ClassMap<Communication> { public CommunicationMap() { Map(x => x.Type).CustomType<CommunicationType>().Not.Nullable(); // 需要改为外键 -> [CommunicationType].[ID] // 其他字段省略 } } public class CommunicationTypeRecordMap : ClassMap<CommunicationTypeRecord> { public CommunicationTypeRecordMap () { Table("CommunicationType"); Id(x => x.ID).Column("ID").GeneratedBy.Assigned(); Map(x => x.Name).Column("Name").Not.Nullable().CustomType("AnsiString"); Map(x => x.Description).Column("Description").Nullable(); } }
注意:也接受非Fluent的NHibernate解决方案。
尝试过的方案及问题
尝试以下配置,能正确生成数据库,但无法完成数据加载/存储:
References<CommunicationTypeRecord>(x => x.CommunicationType).ForeignKey().Not.Nullable();
抛出异常:
[ArgumentException: 你的映射存在问题。可能是尝试将System.ValueType映射到
(NHibernate不允许),或者错误地使用了映射到 的IDictionary。 值类型(CommunicationType)不能用作IdentityKey。关于值类型装箱拆箱的问题以及为何不能用作IdentityKey,相关讨论有详细说明。(Parameter 'key')]
Data:
StackTrace:
at NHibernate.Util.IdentityMap.VerifyValidKey(Object obj)
at NHibernate.Util.IdentityMap.set_Item(Object key, Object value)
at NHibernate.Engine.StatefulPersistenceContext.AddChildParent(Object child, Object parent)
at NHibernate.Engine.Cascade.CascadeToOne(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeAssociation(Object parent, Object child, IType type, CascadeStyle style, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeProperty(Object parent, Object child, IType type, CascadeStyle style, String propertyName, Object anything, Boolean isCascadeDeleteEnabled)
at NHibernate.Engine.Cascade.CascadeOn(IEntityPersister persister, Object parent, Object anything)
at NHibernate.Event.Default.AbstractSaveEventListener.CascadeBeforeSave(IEventSource source, IEntityPersister persister, Object entity, Object anything)
at NHibernate.Event.Default.AbstractSaveEventListener.PerformSaveOrReplicate(Object entity, EntityKey key, IEntityPersister persister, Boolean useIdentityColumn, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
at NHibernate.Event.Default.AbstractSaveEventListener.PerformSave(Object entity, Object id, IEntityPersister persister, Boolean useIdentityColumn, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
at NHibernate.Event.Default.AbstractSaveEventListener.SaveWithGeneratedId(Object entity, String entityName, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.SaveWithGeneratedOrRequestedId(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.EntityIsTransient(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.PerformSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.FireSaveOrUpdate(SaveOrUpdateEvent event)
at NHibernate.Impl.SessionImpl.SaveOrUpdate(Object obj)
解决方案
核心问题:NHibernate不允许直接将值类型(枚举)作为实体引用的映射目标,References仅支持引用类型的实体关联。以下是两种可靠解决方案:
方案1:Fluent NHibernate双属性映射(推荐)
同时维护业务用的枚举属性和NHibernate用的实体引用属性,将两者绑定到同一个数据库字段,既保留枚举的易用性,又维护外键约束:
public class Communication { // 业务代码直接使用的枚举属性 public virtual CommunicationType Type { get; set; } // 仅NHibernate使用的实体引用,设为protected避免业务代码直接操作 protected virtual CommunicationTypeRecord TypeRecord { get; set; } // 其他字段省略 } public class CommunicationMap : ClassMap<Communication> { public CommunicationMap() { // 映射实体引用作为外键,绑定到Type字段 References(x => x.TypeRecord) .Column("Type") .Not.Nullable() .ReadOnly() // 枚举属性控制值,无需修改实体引用 .ForeignKey("FK_Communication_CommunicationType"); // 映射枚举属性,与实体引用共享同一个数据库字段 Map(x => x.Type) .Column("Type") .CustomType<CommunicationType>() .Not.Insert() // 避免重复插入 .Not.Update(); // 避免重复更新 } }
方案2:原生NHibernate XML映射
如果使用XML配置,可通过many-to-one映射实体引用,再在实体中封装枚举属性:
<class name="Communication" table="Communication"> <!-- 其他属性配置 --> <many-to-one name="TypeRecord" column="Type" class="CommunicationTypeRecord" not-null="true" readonly="true"/> </class> <class name="CommunicationTypeRecord" table="CommunicationType"> <id name="ID" column="ID" type="Int32"> <generator class="assigned"/> </id> <property name="Name" column="Name" type="AnsiString" not-null="true"/> <property name="Description" column="Description" type="String"/> </class>
实体代码:
public class Communication { private CommunicationType _type; public virtual CommunicationType Type { get => _type; set { _type = value; TypeRecord = new CommunicationTypeRecord { ID = (int)value }; } } public virtual CommunicationTypeRecord TypeRecord { get; protected set; } // 其他字段省略 }
关键说明
- 不可直接用
References映射枚举,因为枚举是值类型,NHibernate的实体关联仅支持引用类型。 - 双属性方案兼顾业务代码的简洁性和数据库外键约束的完整性,是生产环境最常用的方案。
- 若无需业务层访问
CommunicationTypeRecord,可将该属性设为protected或internal,减少不必要的暴露。
内容的提问来源于stack exchange,提问作者Herman Schoenfeld

