9

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.

xtian
  • 2,908
  • 2
  • 30
  • 43
  • Further experiments reveal that AttributeOverrides collapses both the key and the value column of the map into a single column. – xtian Jan 28 '11 at 15:21
  • I tried with an xml mapping and I managed to get a valid schema, but the sql generated by Hibernate stores each property on a different insert, ending in a primary key constraint violation. – xtian Jan 28 '11 at 15:23
  • Honestly, this is much more than I've time to read and I assume that there are multiple questions included. You get much better support when you can reduce the problem to a single question which is easy to describe. – Stefan Steinegger Apr 05 '11 at 13:23
  • 4
    The question is still the same: "how to map a localized string in Hibernate". I present all my attempts at doing it. Sorry if it is too verbose but I did many attempts... I think this is a problem that many people have to face and I'm surprised that there isn't a best practice (I couldn't find much on google). The version in the 2011.Apr.05 edit doesn't satisfy me still. – xtian Apr 06 '11 at 09:26
  • You don't find much about it because it is not a specific problem. You always have two possibilities: map it as entities or as value types. – Stefan Steinegger Apr 06 '11 at 11:22
  • @Stefan It's not a problem until you analyze it in detail. If you use value types (I think you're talking about embedded components and not custom types here) you soon discover that you can't use Criteria queries or Hibernate Search. If you use entities, you still have the option of using a map of entities or an entity with a map inside, but the latter would have the same issues of the first option (embedded component). So there actually seems to be just one option, which is to use a map, with the locale as key and a specific class as value. Just this knowledge would be something worth sharing – xtian Apr 20 '11 at 10:13
  • 1
    Also the need to write a specific class for every localized attribute is something that should be subject to "optimization". My version of this optimization is to use a common superclass and an empty subclass, but having an empty subclass just to name the table looks like a hack to me, so maybe there is a better solution or maybe this is an area worth investigating: a new @Table annotation at the method level could be the best solution, I don't know. – xtian Apr 20 '11 at 10:18
  • I ran into this today. UGHHHH – JasonG Jul 31 '14 at 13:55

1 Answers1

1

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.

Stefan Steinegger
  • 63,782
  • 15
  • 129
  • 193
  • You're right, I was trying to use the same table for both maps, which sounded nice in the beginning but now I realize this is wrong. – xtian Apr 06 '11 at 09:22
  • In the "independent entity" solution, all localized strings of my application go to the same LocalString table. I didn't want this to happen, but I realize now that I didn't say so in my question. My apologies for not making that clear. – xtian Apr 20 '11 at 10:26
  • The argument "one table vs many tables" is something else that could be worth addressing. – xtian Apr 20 '11 at 10:27