Reputation: 73
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
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 @Where
are hibernate annotations, so you have strayed outside standard JPA annotations either way.
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:
Product
can have many Name
s, but only one name per lang
;Name
is unique for the candidate key {ProductId, Lang}
;LongDesc
;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;
}
Expressing this with JPA & making it work with Hibernate:
abstract
and @Entity
to prevent it being stored directly. Specify the @Table
for clarity, along with the @DiscriminatorFormula
.@org.hibernate.annotations.DiscriminatorOptions(force=true)
- this solves an issue with loading the wrong types into the i18n properties in Product
@Entity
classes such as NameText extends LanguageBasedText
. @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
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