Search code examples
c#nhibernatenhibernate-mapping-by-code

How to map an entity as a class property of another entity using mapping by code (NHibernate)


I want my entity's text properties being saved in several languages. For this I just want to store a FK to the TextContent table (which basically contains only a key) and a separate Translation table which contains a row for each translation.

Here is my database model

Database model

And my corresponding entities

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)
    {
        _language = languageId;
        _text = text;
    }

    public bool Equals(Translation other) ...
    public override bool Equals(object obj) ...
    public override int GetHashCode() ...

    public int LanguageId => _language;
    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;
        _originalLanguage = translations.First().LanguageId;
        _originalText = translations.First().Text;
        _translations = translations;
    }

    public virtual int Id => _id;
    public virtual int OriginalLanguageId => _originalLanguage;
    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;
}

The mappings I tried

public class TextContentMapping : ClassMapping<TextContent>
{
    public TextContentMapping()
    {
        Table("TextContent");

        Id(content => content.Id);
        Property(content => content.OriginalLanguage);
        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));
            });
    }
}

As you may guess the insert works but the select doesn't as my current mapping don't provide a link between _nameId and TextContent Name entity ? Is there a way to do it ? If not what would be a better mappings and domain entities that correspond to my database model?


Solution

  • OneToOne mapping is when the other side has the foreign key. It should be ManyToOne. Using these classes

    public class TextContent
    {
        // ORM-only constructor
        protected TextContent() { }
        public TextContent(int languageId, string text) : this()
        {
            OriginalLanguageId = languageId;
            OriginalText = text;
            Translations = new Dictionary<int, string>();
        }
    
        public virtual int Id { get; protected set; }
        public virtual int OriginalLanguageId { get; protected set; }
        public virtual string OriginalText { get; protected set; }
        public virtual IDictionary<int, string> Translations { get; protected set; }
    }
    
    public class Product
    {
        // ORM-only constructor
        protected Product() { }
    
        public Product(TextContent name, TextContent description)
        {
            Name = name;
            Description = description;
        }
    
        public virtual int Id { get; protected set; }
        public virtual TextContent Name { get; protected set; }
        public virtual TextContent Description { get; protected set; }
    }
    

    and these mappings

    public class TextContentMapping : ClassMapping<TextContent>
    {
        public TextContentMapping()
        {
            Table("TextContent");
    
            Id(content => content.Id, m => m.Generator(Generators.Native));
            Property(content => content.OriginalLanguageId);
            Property(content => content.OriginalText);
    
            Map(
                content => content.Translations,
                mapper =>
                {
                    mapper.Table("Translation");
                    mapper.Cascade(Cascade.All);
                    mapper.Key(
                        keyMapper =>
                        {
                            keyMapper.Column(columnMapper => columnMapper.Name("TextContentId"));
                            keyMapper.NotNullable(true);
                        });
                },
                keymapping => keymapping.Element(m => m.Column("LanguageId")),
                elementMapping => elementMapping.Element(m => m.Column("Text"))
                );
        }
    }
    
    public class ProductMapping : ClassMapping<Product>
    {
        public ProductMapping()
        {
            Table(nameof(Product));
    
            Id(product => product.Id, m => m.Generator(Generators.Native));
    
            ManyToOne(
                product => product.Name,
                mapper =>
                {
                    mapper.Column("NameId");
                    mapper.Cascade(Cascade.All);
                    mapper.NotNullable(true);
                });
    
            ManyToOne(
                product => product.Description,
                mapper =>
                {
                    mapper.Column("DescriptionId");
                    mapper.Cascade(Cascade.All);
                    mapper.NotNullable(true);
                });
        }
    }
    

    the following code works

            session.Save(new Product(new TextContent(1, "someText") { Translations = { { 2, "translated Text" } } }, new TextContent(1, "some description")));
            session.Flush();
            session.Clear();
    
            var p = session.Query<Product>().ToList();
            var s = p[0].Name.Translations[2];