You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过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.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 19:37:07