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:
- The current Criteria API doesn't work on embedded collections: http://opensource.atlassian.com/projects/hibernate/browse/HHH-3646
- The current Hibernate Search (Lucene) API doesn't work on embedded collections either: http://opensource.atlassian.com/projects/hibernate/browse/HSEARCH-566
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.