Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to internationalize a Hibernate entity

I'm trying to add internationalization (multiple languages) support to java entities. I'm open to any options with as few boilerplate code as possible when adding translation to each new field. I am not limited to JPA, can use hibernate annotations as well. In worst case plain sql will suit as well. Possibly there are some ready libraries that I haven't found. It should not necessary follow my idea described below.

Ideally I need the database to look like this:

i18n
+------+--------+------+
|  id  | locale | text |
+------+--------+------+
|  1   |   en   | foo  |
+------+--------+------+
|  1   |   de   | bar  |
+------+--------+------+
|  2   |   en   | foo2 |
+------+--------+------+
|  2   |   de   | bar2 |
+------+--------+------+

parent
+------+------+
|  id  | text |
+------+------+
|  99  |   1  |
+------+------+
|  100 |   2  |
+------+------+

i18n is a table that should contain just 3 columns: id, locale and text. Table parent has a column text (if there is just single field that requires i18n, more columns otherwise) that contains values from i18n.id. I tried the following mapping in the Parent class:

@ElementCollection @CollectionTable(name="i18n", joinColumns = @JoinColumn(referencedColumnName="id"))
@MapKeyColumn(name="locale") @Column(name="text")
public Map<String, String> text = newHashMap();

It seems to work when DDL generation is disabled and I create tables by myself, but when DDL generation is enabled, it generates an unnecessary column i18n.parent_id and a constraint for it:

ALTER TABLE PUBLIC.I18N ADD CONSTRAINT 
PUBLIC.FK_HVGN9UJ4DJOFGLT8L78BYQ75I FOREIGN KEY(PARENT_ID) INDEX 
PUBLIC.FK_HVGN9UJ4DJOFGLT8L78BYQ75I_INDEX_2 REFERENCES 
PUBLIC.PARENT(ID) NOCHECK

How can I get rid of this extra column? Is it possible to avoid having a reference from i18n table to parent table? This link makes it difficult to reuse the i18n table. I either need to hold some discriminator value in i18n table or use GUID throughout the database, as id's in different tables will clash. First option means lots of boilerplate code. Second option means a lot of work to be done in the current project.

I need a reusable way to add i18n to entity. My parent classes will look approximately like this. And there will be several such parent classes with different set of fields that must be internationalized.

@Entity
public class Parent {

    @Id @GeneratedValue
    public Long id;

    public String title; // must be in internationalized
    public String text; // must be in internationalized
    public String details; // must be in internationalized

    // ... other fields
}
like image 996
user1603038 Avatar asked Sep 26 '16 13:09

user1603038


3 Answers

On the database level collection instances are distinguished by the foreign key of the entity that owns the collection. This foreign key is referred to as the collection key column, or columns, of the collection table.

So I suppose you want to disable the forgien key generation for your propose, simplely you can do it with this

@Entity
public class Parent {

    @Id
    @GeneratedValue
    public Long id;

    @ElementCollection
    @CollectionTable(name = "i18n", foreignKey = @ForeignKey(name = "none"), joinColumns = @JoinColumn(name = "id"))
    @MapKeyColumn(name = "locale")
    @Column(name = "text")
    public Map<String, String> text = new HashMap<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Map<String, String> getText() {
        return text;
    }

    public void setText(Map<String, String> text) {
        this.text = text;
    }
}

Then when you verify the generated DDL, the forgien key it not applyed to the I18N table, but still get the ability

@Test
public void testParent() {
    Parent p = new Parent();
    HashMap<String, String> text = new HashMap<>();
    text.put("en", "foo");
    text.put("de", "bar");
    p.setText(text);

    entityManager.persist(p);
    entityManager.flush();

    Parent parent = entityManager.find(Parent.class, p.getId());
    System.out.println("en: " + parent.getText().get("en"));
    System.out.println("de: " + parent.getText().get("de"));
}

About is a simple test( Spring Boot 1.4 release ), and will see the output in the console:

Hibernate: 
    create table i18n (
        id bigint not null,
        text varchar(255),
        locale varchar(255) not null,
        primary key (id, locale)
    )
Hibernate: 
    create table parent (
        id bigint generated by default as identity,
        primary key (id)
    ) 
......
Hibernate: 
    insert 
    into
        parent
        (id) 
    values
        (null)
Hibernate: 
    insert 
    into
        i18n
        (id, locale, text) 
    values
        (?, ?, ?)
Hibernate: 
    insert 
    into
        i18n
        (id, locale, text) 
    values
        (?, ?, ?)
en: foo
de: bar

This is the table in H2 db:

enter image description here

like image 130
Liping Huang Avatar answered Nov 13 '22 04:11

Liping Huang


Seems that Hibernate does not provide any i18n support out of the box, so you on the right way to implement custom solution for this. I can also assume that your goal is to add localization support to an existing project with minimal cost.

I can suggest you to use ManyToMany relation on Parent and i18n tables.

In this case you are fully independent on Parent and i18n tables structure as you want, but there is some overhead with additional join tables per each "Parent", which contains PK reference pairs form i18n and "Parent" table. Also with ManyToMany approach records form i18n can be reused in different "Parent" tables.

So your table structure may look like this:

i18n
+------+--------+------+
|  id  | locale | text |
+------+--------+------+
|  1   |   en   | foo  |
+------+--------+------+
|  2   |   de   | bar  |
+------+--------+------+
|  3   |   en   | foo2 |
+------+--------+------+
|  4   |   de   | bar2 |
+------+--------+------+

i18n_parent
+-------------+---------+
|   text_id   | i18n_id |
+-------------+---------+
|      1      |   1     |
+------+------+---------+
|      1      |   2     |
+------+------+---------+
|      2      |   3     |
+------+------+---------+
|      2      |   4     |
+------+------+---------+


parent
+------+------+
|  id  | text |
+------+------+
|  99  |   1  |
+------+------+
|  100 |   2  |
+------+------+

Entity code example:

@Entity
public class Parent {

    @Id
    @GeneratedValue
    public Long id;

    @ManyToMany
    @JoinTable(name = "i18n_parent",
            joinColumns = @JoinColumn(name = "text_id"),
            inverseJoinColumns = @JoinColumn(name = "i18n_id"))
    @MapKey(name = "locale")
    private Map<String, LocalizedTextEntity> text = new HashMap<>();

    .....    
}

@Entity
@Table(name = "i18n")
public class LocalizedTextEntity {

    @Id
    @GeneratedValue
    public Long id;

    @Column(name = "locale")
    private String locale;

    @Column(name = "text")
    private String text;

    .....
}
like image 1
Sergey Bespalov Avatar answered Nov 13 '22 05:11

Sergey Bespalov


Look like you are searching for hibernate composite primary key. You should perform this trick:

@Embeddable
public class LocaleForeignKey {
    Integer id;
    String locale;
}

@Entity
public class I18n {
    @AttributeOverrides({
      @AttributeOverride(name="id", column = @Column(name="id"))
      @AttributeOverride(name="locale", column = @Column(name="locale"))
    })
    @EmbeddedId
    LocaleForeignKey id;

    String text;
    ...getters-setters
}

Unfortunately, I have no idea how to map it as 'locale' map, but think it possible with @JoinColumn annotations, or try to follow to @Alan Hay post.

like image 1
degr Avatar answered Nov 13 '22 04:11

degr