如何通过NHibernate代码映射实现实体作为另一实体的类属性
多语言文本属性的NHibernate映射问题
需求
实体的文本属性需支持多语言存储,仅存储TextContent表的外键,通过独立Translation表存储各语言翻译。
数据库模型说明
Product表:包含Id、NameId(外键关联TextContent.Id)、DescriptionId(外键关联TextContent.Id)字段TextContent表:包含Id、OriginalLanguageId、OriginalText字段Translation表:包含TextContentId(外键关联TextContent.Id)、LanguageId、Text字段,TextContentId+LanguageId为联合主键
领域实体类
public sealed class Translation : IEquatable<Translation> { public static bool operator ==(Translation left, Translation right) => Equals(left, right); public static bool operator !=(Translation left, Translation right) => !Equals(left, right); // ORM-only constructor private Translation() { } public Translation(int languageId, string text) { _languageId = languageId; _text = text; } public bool Equals(Translation other) ... public override bool Equals(object obj) ... public override int GetHashCode() ... public int LanguageId => _languageId; public string Text => _text; private readonly int _languageId; private readonly string _text; } public class TextContent { // ORM-only constructor protected TextContent() { } public TextContent(int Id, List<Translation> translations) { _id = Id; _originalLanguageId = translations.First().LanguageId; _originalText = translations.First().Text; _translations = translations; } public virtual int Id => _id; public virtual int OriginalLanguageId => _originalLanguageId; public virtual string OriginalText => _originalText; public virtual IList<Translation> Translations => _translations; private readonly int _id; private readonly int _originalLanguageId; private readonly string _originalText; private readonly IList<Translation> _translations; } public partial class Product { // ORM-only constructor protected Product() { } public Product(int Id, TextContent name, TextContent description) { _id = Id; _nameId = name.Id; _name = name; _descriptionId = description.Id; _description = description; } public virtual int Id => _id; public virtual TextContent Name => _name; public virtual TextContent Description => _description; private int _id; private int _nameId; private TextContent _name; private int _descriptionId; private TextContent _description; }
尝试的NHibernate映射代码
public class TextContentMapping : ClassMapping<TextContent> { public TextContentMapping() { Table("TextContent"); Id(content => content.Id); Property(content => content.OriginalLanguageId); Property(content => content.OriginalText); Bag( content => content.Translations, mapper => { mapper.Table(nameof(Translation)); mapper.Cascade(Cascade.All); mapper.Key( keyMapper => { keyMapper.Column(columnMapper => columnMapper.Name("TextContentId")); keyMapper.NotNullable(true); }); }, relation => relation.Component( mapper => { mapper.Property(translation => translation.LanguageId); mapper.Property(translation => translation.Text); })); } } public class ProductMapping : ClassMapping<Product> { public ProductMapping() { Table(nameof(Product)); Id(product=> product.Id, mapper => mapper.Access(Accessor.Field)); Property( "_nameId", mapper => { mapper.Column("NameId"); mapper.Access(Accessor.Field); mapper.NotNullable(true); }); Property( "_descriptionId", mapper => { mapper.Column("DescriptionId"); mapper.Access(Accessor.Field); mapper.NotNullable(true); }); OneToOne( product => product.Name, mapper => { mapper.Constrained(true); mapper.Cascade(Cascade.All); mapper.Access(Accessor.Field); mapper.Class(typeof(TextContent)); }); OneToOne( product => product.Description, mapper => { mapper.Constrained(true); mapper.Cascade(Cascade.All); mapper.Access(Accessor.Field); mapper.Class(typeof(TextContent)); }); } }
问题
插入操作正常,但查询失败,原因是当前映射未建立_nameId与TextContent Name实体的关联。请问是否有解决方法?若没有,适配该数据库模型的更佳映射方案与领域实体设计是什么?
解决方案
一、修复当前映射的问题
当前用OneToOne映射Name和Description是错误的——这是多对一关联(多个Product可关联同一个TextContent),而非一对一。需要替换为ManyToOne并指定外键字段:
修改后的ProductMapping
public class ProductMapping : ClassMapping<Product> { public ProductMapping() { Table(nameof(Product)); Id(product=> product.Id, mapper => mapper.Access(Accessor.Field)); // 替换OneToOne为ManyToOne,直接关联外键列 ManyToOne( product => product.Name, mapper => { mapper.Column("NameId"); mapper.Access(Accessor.Field); mapper.Class(typeof(TextContent)); mapper.NotNullable(true); mapper.Cascade(Cascade.All); }); ManyToOne( product => product.Description, mapper => { mapper.Column("DescriptionId"); mapper.Access(Accessor.Field); mapper.Class(typeof(TextContent)); mapper.NotNullable(true); mapper.Cascade(Cascade.All); }); } }
同时移除原来单独映射_nameId、_descriptionId的代码——ManyToOne会自动处理外键字段,重复映射会导致冲突。修改后NHibernate就能通过外键关联查询TextContent实体,解决查询失败问题。
二、更佳的领域实体与映射设计
1. 领域实体优化
移除Product中的_nameId、_descriptionId私有字段——外键是持久化细节,领域实体应专注业务逻辑,无需暴露这些内容:
public partial class Product { protected Product() { } // 移除Id参数,由ORM自动生成赋值 public Product(TextContent name, TextContent description) { _name = name; _description = description; } public virtual int Id => _id; public virtual TextContent Name => _name; public virtual TextContent Description => _description; private int _id; private TextContent _name; private TextContent _description; }
2. 映射优化
如果Translation需要独立管理(比如单独查询),可将其从值对象改为实体类,映射联合主键:
Translation实体调整
public sealed class Translation : IEquatable<Translation> { public static bool operator ==(Translation left, Translation right) => Equals(left, right); public static bool operator !=(Translation left, Translation right) => !Equals(left, right); private Translation() { } public Translation(int languageId, string text, TextContent textContent) { _languageId = languageId; _text = text; _textContent = textContent; } public bool Equals(Translation other) ... public override bool Equals(object obj) ... public override int GetHashCode() ... public int LanguageId => _languageId; public string Text => _text; public virtual TextContent TextContent => _textContent; private readonly int _languageId; private readonly string _text; private readonly TextContent _textContent; }
TranslationMapping配置
public class TranslationMapping : ClassMapping<Translation> { public TranslationMapping() { Table(nameof(Translation)); // 配置联合主键:TextContentId + LanguageId CompositeId() .KeyManyToOne(t => t.TextContent, km => km.Column("TextContentId")) .KeyProperty(t => t.LanguageId); Property(t => t.Text); } }
TextContentMapping调整
将原来的Component集合改为OneToMany关联:
public class TextContentMapping : ClassMapping<TextContent> { public TextContentMapping() { Table("TextContent"); Id(content => content.Id); Property(content => content.OriginalLanguageId); Property(content => content.OriginalText); Bag( content => content.Translations, mapper => { mapper.Table(nameof(Translation)); mapper.Cascade(Cascade.All); mapper.Inverse(true); mapper.Key(km => km.Column("TextContentId")); }, relation => relation.OneToMany()); } }
这种设计的优势:
- 领域实体更纯净,无持久化细节干扰
- 映射完全贴合数据库结构,避免冗余配置
- 扩展性更强,后续单独管理翻译内容时无需大幅调整
内容的提问来源于stack exchange,提问作者Alexandre A.
相关产品推荐
相关产品推荐

