Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping a localized string in Hibernate - any best practice?

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.

like image 324
xtian Avatar asked Jan 24 '11 16:01

xtian


1 Answers

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.

like image 144
Stefan Steinegger Avatar answered Nov 04 '22 16:11

Stefan Steinegger