I'm writing an application where all String properties must be localized, i.e. they must store a different value for every available Locale. A quick solution would be to use a Map, which can be easily mapped in Hibernate but isn't nice on the Java programmer:
public class Product {
private Map<Locale, String> description;
private Map<Locale, String> note;
I therefore implemented a LocalString object that can hold different strings for different locales:
public class LocalString {
private Map<Locale, String> localStrings;
The domain object becomes
public class Product {
private LocalString description;
private LocalString note;
How do I best map these objects with Hibernate annotations?
I think the best mapping would be done using LocalString as a component:
@Embeddable
public class LocalString {
private Map<Locale, String> localStrings;
@ElementCollection
public Map<Locale, String> getLocalStrings() {
return localStrings;
}
...
@Entity
public class Product {
private Long id;
private LocalString description;
private LocalString note;
@Embedded
public LocalString getDescription() {
return description;
}
All is fine so far: the hbm2ddl ant task creates two tables, a "Products" table and a "Products_localStrings" table which contains key and value columns. Everything breaks when I add the getter for the second property:
@Embedded
public LocalString getNote() {
return note;
}
The second property doesn't show up in the schema. I tried using the @AttributesOverride tag to define different names for the two columns, but the generated schema is not correct:
@Embedded
@AttributeOverrides({
@AttributeOverride(name="localStrings", column=@Column(name="description"))
})
public LocalString getDescription() {
return description;
}
@Embedded
@AttributeOverrides({
@AttributeOverride(name="localStrings", column=@Column(name="note"))
})
public LocalString getNote() {
return note;
}
In the generated schema, the key column has disappeared and the primary key uses "description", which is not correct:
create table Product_localStrings (Product_id bigint not null, note varchar(255), description varchar(255), primary key (Product_id, description));
Any way to fix this?
Would I be better off without embedded components, using LocalString as an Entity?
Any alternative designs?
Thank you.
EDIT
I tried with a xml mapping and I managed to get a proper schema, but inserts fail with a primary key violation because hibernate generates two inserts instead of just one
<hibernate-mapping>
<class name="com.yr.babka37.demo.entity.Libro" table="LIBRO">
<id name="id" type="java.lang.Long">
<column name="ID" />
<generator class="org.hibernate.id.enhanced.SequenceStyleGenerator"/>
</id>
<property name="titolo" type="java.lang.String">
<column name="TITOLO" />
</property>
<component name="descrizioni" class="com.yr.babka37.entity.LocalString">
<map name="localStrings" table="libro_strings" lazy="true" access="field">
<key>
<column name="ID" />
</key>
<map-key type="java.lang.String"></map-key>
<element type="java.lang.String">
<column name="descrizione" />
</element>
</map>
</component>
<component name="giudizi" class="com.yr.babka37.entity.LocalString">
<map name="localStrings" table="libro_strings" lazy="true" access="field">
<key>
<column name="ID" />
</key>
<map-key type="java.lang.String"></map-key>
<element type="java.lang.String">
<column name="giudizio" />
</element>
</map>
</component>
</class>
</hibernate-mapping>
The schema is
create table LIBRO (ID bigint not null auto_increment, TITOLO varchar(255), primary key (ID));
create table libro_strings (ID bigint not null, descrizione varchar(255), idx varchar(255) not null, giudizio varchar(255), primary key (ID, idx));
alter table libro_strings add index FKF576CAC5BCDBA0A4 (ID), add constraint FKF576CAC5BCDBA0A4 foreign key (ID) references LIBRO (ID);
Log:
DEBUG org.hibernate.SQL - insert into libro_strings (ID, idx, descrizione) values (?, ?, ?)
DEBUG org.hibernate.SQL - insert into libro_strings (ID, idx, giudizio) values (?, ?, ?)
WARN o.h.util.JDBCExceptionReporter - SQL Error: 1062, SQLState: 23000
ERROR o.h.util.JDBCExceptionReporter - Duplicate entry '5-ita_ITA' for key 'PRIMARY'
How do I tell hibernate to generate just a single insert like the following?
insert into libro_strings (ID, idx, descrizione, giudizio) values (?, ?, ?, ?)
EDIT on 2011.Apr.05
I've been using the Map solution for a while (annotated with @ElementCollection), until I stumbled upon two problems:
I know there are many workarounds, like using HQL instead of Criteria and defining your own FieldBridge to take care of the Map in Lucene, but I don't like workarounds: they work until the next problem comes around. So I'm now following this approach:
I define a class "LocalString" that holds locale and value (the Locale is actually the ISO3 code):
@MappedSuperclass
public class LocalString {
private long id;
private String localeCode;
private String value;
Then I define, for each property I want to localize, a subclass of LocalString, which is empty:
@Entity
public class ProductName extends LocalString {
// Just a placeholder to name the table
}
Now I can use it in my Product object:
public class Product {
private Map<String, ProductName> names;
@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)
@JoinColumn(name="Product_id")
@MapKey(name="localeCode")
protected Map<String, ProductName> getNames() {
return names;
}
With this approach I have to write an empty class for each property that I need to localize, which is needed to create a unique table for that property. The benefit is that I can use Criteria and Search without restrictions.
Your xml mapping doesn't work because you mapped value types into the same table. When using element or composite-element, the data is treated as value types and belong to the class where it is contained in. It requires its own table. The id is only unique within the collection.
Either map it as component:
<component name="descrizioni">
<map name="localStrings" table="descrizioni_strings" ...>
<!-- ... -->
</map>
</component>
<component name="giudizi">
<map name="localStrings" table="giudizi_strings" ... >
<!-- ... -->
</map>
</component>
Or as independent entity:
<many-to-one name="descrizioni" class="LocalString"/>
<many-to-one name="giudizi" class="LocalString"/>
In the second case, LocalString is an entity. It needs an id and its own mapping definition.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With