DavidB
DavidB

Reputation: 73

Hibernate multiple Many To many relation with the same class

I got a problem with hibernate and mapping multiple objects of the same type, maybe im just stupid.

I have an Product

@Entity
@Table(name = "product")
public class Product {

  @Id
  private int id;

  @ManyToMany(cascade = CascadeType.ALL)
  private List<LanguageBasedText> name;

  @ManyToMany(cascade = CascadeType.ALL)
  private List<LanguageBasedText> longDesc;

 (...)
}

and the Language Based Text

@Entity
@Table(name = "languageBasedText")
public class LanguageBasedText {

  @EmbeddedId
  private LanguageTextEmbeddedKey embeddedID;

  @Column(name = "text")
  private String text;

}

with an embedded Key

@Embeddable
public class LanguageTextEmbeddedKey implements Serializable {

  @Column(name = "productID")
  private int productID;
  @Column(name = "lang")
  private String lang;
  @Column(name = "type")
  private String type; 
  //i get the type from an other source and it is the information where 
  //the text goes (name or longDesc or 100 other posibil multi 
  //language text fields)
}

the question ist: How do i get hibernate to map this right

i want a table like

TEXT

product_id | lang | type | text

and

PRODUCT

product_id | some other stuff |

Upvotes: 0

Views: 3719

Answers (2)

Andy Brown
Andy Brown

Reputation: 19171

Summary: You don't actually want many-to-many, you want one-to-many with inheritance (using the Single-Table-Per-Hierarchy strategy) on the many side, or a filter on the OneToMany properties using @Where. The former approach is possibly "more OO", the latter is less typing and potentially prevents class explosion if you have many different i18n text fields. Both @DiscriminatorOptions and @Whereare hibernate annotations, so you have strayed outside standard JPA annotations either way.

The Model

If you think about the ManyToMany approach you can see that without a filter to specify a type='name' criteria, name could pull back a longDesc text as well as a name text for the same language - which can't be right. So if you apply a filter then you now have OneToMany as you only have one name per {productID, lang}.

If you think of this in pure modelling terms:

  • one Product can have many Names, but only one name per lang;
  • so each Name is unique for the candidate key {ProductId, Lang};
  • and the same is then true of LongDesc;
  • You can therefore view Name and LongDesc as separate business entities.

This is therefore a OneToMany model for each of the LanguageBasedText properties in Product. Name, LongDesc (and other similar properties you allude to) just happen to share the same properties: they implement the same interface so storing them in one table with a discriminator is possible.

Inheritance strategies

You can take advantage of the common interface for the text entities and implement an inheritance strategy. In your case, with a discriminator column, you jumped ahead and are naturally using a Single-Table-Per-Hierarchy inheritance model. In your code you effectively have:

public abstract class LanguageBasedText {}
public class NameText extends LanguageBasedText {}
public class LongDescText extends LanguageBasedText {}
public class Product {
  private Set<NameText> names;
  private Set<LongDescText> longDescs;
}

The code

Expressing this with JPA & making it work with Hibernate:

  1. Create the root entity type, usually as abstract and @Entity to prevent it being stored directly. Specify the @Table for clarity, along with the @DiscriminatorFormula.
  2. Also add @org.hibernate.annotations.DiscriminatorOptions(force=true) - this solves an issue with loading the wrong types into the i18n properties in Product
  3. Inherit that in @Entity classes such as NameText extends LanguageBasedText.
  4. Add the @OneToMany properties into Product, typed as collections parameterized for the text class subtype (e.g. Set<NameText>).

And in code:

@Entity @Table(name = "product")
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    Integer id;

    private Product() {}
    public Product(String sku) {
        this.sku = sku;
    }

    // omitting the other properties
    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "productID", insertable = false, updatable = false)
    private Set<NameText> name = new HashSet<>();

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "productID", insertable = false, updatable = false)
    private Set<LongDescText> longDesc = new HashSet<>();

    private String sku;
    ...
}

@Embeddable public class LanguageTextEmbeddedKey implements Serializable {
    private LanguageTextEmbeddedKey(){}
    public LanguageTextEmbeddedKey(int productID, String lang, String type) {
        this.productID = productID;
        this.lang = lang;
        this.type = type;
    }

    @Column(name = "productID") private int productID;
    @Column(name = "lang") private String lang;
    @Column(name = "type") private String type;
    ...
}

@Entity @Table(name = "languageBasedText")
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula(value="type")
@org.hibernate.annotations.DiscriminatorOptions(force=true)
public abstract class LanguageBasedText {   
    @EmbeddedId private LanguageTextEmbeddedKey embeddedID;
    @ManyToOne() @JoinColumn(name="productID", insertable=false,updatable=false, nullable=false)
    protected Product p;
    private String text;

    protected LanguageBasedText(){}
    protected LanguageBasedText(int productID, String lang, String type, String text) {
        this.embeddedID = new LanguageTextEmbeddedKey(productID, lang, type);
        this.text = text;
    }
    ... 
}

@Entity @DiscriminatorValue("name")
public class NameText extends LanguageBasedText {
    private NameText(){}
    public NameText(int productID, String lang, String text) {
        super(productID, lang, "name", text);
    }
}
// similarly for LongDescText

N.B. There is a lot of other (less relevant) code missing from the above, including the mandatory no-args ctors on all types, mutators/accessors etc.. I have also used productID in the ctors for the text classes, but I would recommend you replace those ctors with DDD methods such as addNameText to encapsulate that id.

Upvotes: 2

DuncanKinnear
DuncanKinnear

Reputation: 4643

For a start, the associations in Product are not @ManyToMany. A LanguageBasedText Entity can only refer to one Product, therefore the associations in Product must be @OneToMany.

But, what I think you are trying to achieve is to have the name list in Product contain the list of LanguageBasedText objects that refer to that Product and have a Type of "name". And the longDesc list should contain the LanguageBasedText objects that have the type of "longDesc".

Since you are using Hibernate, the easiest solution for that would be to use the @Where annotation on the @OneToMany lists, thus in Product you would have:

@OneToMany(cascade = CascadeType.ALL)
@Where(clause="type='name'")
private List<LanguageBasedText> names;

@OneToMany(cascade = CascadeType.ALL)
@Where(clause="type='longDesc'")
private List<LanguageBasedText> longDescs;

Notice I also changed your lists to be plurals, as that makes more sense (in English).

Another (more complex) solution would be to use inheritance and define sub-classes of LanguageBasedText for each type, using a single table strategy and with the type column as the discriminator value. Thus you would have sub-classes like:

@Entity
@DiscriminatorValue("name")
public class NameLanguageBasedText {
    ... etc

But you would end up with hundreds of these sub-classes, and I'm not sure how the discriminator column would work being in an Embedded Id.

Upvotes: 1

Related Questions