Search code examples
jpaeclipselink

Modeling implicit many to many relations in JPA


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);
    }
}

Solution

  • 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