0

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?

Alexandre A.
  • 1,619
  • 18
  • 29

1 Answers1

1

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];
Firo
  • 30,626
  • 4
  • 55
  • 94