0

Consider the following legacy data model which I want to represent in JPA:

  • Languages have id and name.
  • Language Descriptions have a composite primary key consisting of an id and a language_id and, additionally, a description such that for each translation there is one entry for every language.
  • Articles have (internationalized) descriptions, identified by the aforementioned languagedescription_id.

Now, my first approach was to model the entities like

@Entity
public class Language {

    @Id
    @Column(length = 3)
    private String id;

    private String languageDescription;

    // ...
}

@Embeddable
public class LanguageDescriptionId implements Serializable {

    @Column(length = 9)
    private String id;

    @Column(length = 3)
    private String languageId;

    // ...
}

@Entity
public class LanguageDescription {

    @EmbeddedId
    private LanguageDescriptionId languageDescriptionId;

    @ManyToOne
    @MapsId(value = "languageId")
    private Language language;

    @Column(length = 60)
    private String description;

    // ...
}

@Entity
public class Article {

    @Id
    private String id;

    @ManyToOne
    private LanguageDescription languageDescription;
}

which a) is not correct, because technically articles can have a list of translations (ideally being a mapping of language to language descriptions to easily fetch a translation for a given language) and b) leads to the database situation, that my ORM (EclipseLink) added both primary key columns of languagedescription to the article table, although articles are only referencing the languagedescription_id to prevent multiple redundant entries. When replacing the Article entity with

@Entity
public class Article {

    @Id
    private String id;

    @ManyToMany
    private List<LanguageDescription> languageDescriptions;
}

my ORM creates a mapping table which I would have to populate by hand and which would extend the (legacy) data model with redundant information. At the moment my approach is to model the Article entity like

@Entity
public class Article {

    @Id
    private String id;

    @Column(length = 9)
    private String languageDescriptionId;
}

while omitting the JPA support and fetching the translations in a @EntityListener.

Now, is this situation somehow manageable automatically in JPA or are these kind of "implicit" many to many relations without a mapping table not supported in JPA at all?

Example

public class Main {

    public static void main(String[] args) {
        final Language english = createLanguage("language_1", "English");
        final Language german = createLanguage("language_2", "German");

        createArticle1(english, german);
        createArticle2(english, german);
    }

    private static Language createLanguage(String id, String languageDescription) {
        final Language language = new Language();
        language.setId(id);
        language.setLanguageDescription(languageDescription);

        return language;
    }

    private static void createArticle1(Language english, Language german) {
        final String languageDescriptionId = "languagedescription_1";

        final LanguageDescription languageDescription1 = new LanguageDescription();
        final LanguageDescriptionId languageDescriptionId1 = new LanguageDescriptionId();
        languageDescriptionId1.setId(languageDescriptionId);
        languageDescriptionId1.setLanguageId(english.getId());
        languageDescription1.setLanguageDescriptionId(languageDescriptionId1);
        languageDescription1.setLanguage(english);
        languageDescription1.setDescription("Engine");

        final LanguageDescription languageDescription2 = new LanguageDescription();
        final LanguageDescriptionId languageDescriptionId2 = new LanguageDescriptionId();
        languageDescriptionId2.setId(languageDescriptionId);
        languageDescriptionId2.setLanguageId(german.getId());
        languageDescription2.setLanguageDescriptionId(languageDescriptionId2);
        languageDescription2.setLanguage(german);
        languageDescription2.setDescription("Motor");

        final Article article1 = new Article();
        article1.setId("a_1");
        article1.setLanguageDescriptionId(languageDescriptionId);
    }

    private static void createArticle2(Language english, Language german) {
        final String languageDescriptionId = "languagedescription_2";

        final LanguageDescription languageDescription3 = new LanguageDescription();
        final LanguageDescriptionId languageDescriptionId3 = new LanguageDescriptionId();
        languageDescriptionId3.setId(languageDescriptionId);
        languageDescriptionId3.setLanguageId(english.getId());
        languageDescription3.setLanguageDescriptionId(languageDescriptionId3);
        languageDescription3.setLanguage(english);
        languageDescription3.setDescription("Turn Signal");

        final LanguageDescription languageDescription4 = new LanguageDescription();
        final LanguageDescriptionId languageDescriptionId4 = new LanguageDescriptionId();
        languageDescriptionId4.setId(languageDescriptionId);
        languageDescriptionId4.setLanguageId(german.getId());
        languageDescription4.setLanguageDescriptionId(languageDescriptionId4);
        languageDescription4.setLanguage(german);
        languageDescription4.setDescription("Blinker");

        final Article article2 = new Article();
        article2.setId("a_2");
        article2.setLanguageDescriptionId(languageDescriptionId);
    }
}
Smutje
  • 17,733
  • 4
  • 24
  • 41
  • Your database model is unclear. Why have a composite PK for language LanguageDescription, isn't LanguageDescriptionId.id unique? Is it meant as a foreign key to article, or how is its value set/determined? ManyToMany mappings use a join table, but you can specify a OneToMany with a joincolumn instead. JPA forces PKs to go to primary keys, and while it is recommended for caching and numerous performance reasons, EclipseLink has support for FK to non-pk fields in mappings. – Chris Mar 30 '16 at 13:22
  • "Why have a composite PK for language LanguageDescription, isn't LanguageDescriptionId.id unique?" - no, one LanguageDescription ID maps to a list of translations with one translation for every language made unique by the combination of LanguageDescription ID and language ID. "Is it meant as a foreign key to article, or how is its value set/determined? " - articles have LanguageDescription ID in the legacy data model and point to a set of LanguageDescriptions. Also, @OneToMany is not suitable as translations can be re-used over articles and I don't want to create an additional join table. – Smutje Mar 30 '16 at 14:03
  • You will need to explain or show your tables, as it isn't clear how this relates to a translation. Nor is it clear how you plan to set the languageDescriptionId fields and 'modify' your lists. I think your second option, of using a String for languageDescriptionId in Article is your best option, and then just query for the collection of associated LanguageDescription's with that matching languageId is your best bet rather then adding complicated and artificial ManyToMany rules to your model. Caching the relationships in your model isn't always the best solution when you can just query them – Chris Mar 30 '16 at 16:10
  • I added an example to depict my problem for the sake of completeness as can be seen that both of my articles get one translation for English and German respectively and the only relation between articles and translations is having the same `languageDescriptionId` but no object relation - now, when one wanted to know the translation for one of the articles, one has to get all `LanguageDescription` objects with the same `languageDescriptionId` as the article and filter the one with the desired language. – Smutje Mar 31 '16 at 05:59
  • As mentioned, the article->LanguageDescription relationship is a 1:M, not a many to many, as the article has a single languageDescriptionId string value that the LanguageDescription instances reference. How does translations come into it and why would it require a join table? – Chris Mar 31 '16 at 12:46
  • Yes, one article has one languageDescriptionId, but this languageDescriptionId is contained in a number of language descriptions, that's why I first modeled the relation between `Article` and `LanguageDescription` as `@ManyToMany`. – Smutje Mar 31 '16 at 13:29

1 Answers1

0

Something like the following is possible:

@Entity
public class Article {

    @Id
    private String id;

    @Column(length = 9)
    private String languageDescriptionId;

    @OneToMany
    @JoinColumn(name="ID", referencedColumnName="LANGUAGEDESCRIPTIONID", insertable=false, updatable=false)
    private List<LanguageDescription> languageDescriptions;
}

Now the above OneToMany is not JPA compliant, as LanguageDescription has a composite PK, so EclipseLink and other providers will throw validation exceptions, it is just an example of what it will look like. You are going to need to either make it transient or add another join column, just to get around the validation, and then modify or add the mapping in a descriptor customizer. This is described and shown somewhat in the answers here jpa, eclips-link 2.5.1: OneToMany not working on columns not primary key

Community
  • 1
  • 1
Chris
  • 20,138
  • 2
  • 29
  • 43
  • Alright thanks - as the correct translation is indeed `@Transient` at the moment and being loaded via `@EntityListener` using the user sessions' `Language` to determine the target translation, I will accept your answer. – Smutje Mar 31 '16 at 13:33