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
}
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:
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;
.....
}
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.
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