【问题标题】:How to internationalize a Hibernate entity如何国际化 Hibernate 实体
【发布时间】:2017-02-03 21:48:46
【问题描述】:

我正在尝试向 java 实体添加国际化(多语言)支持。在向每个新字段添加翻译时,我愿意接受尽可能少的样板代码的任何选项。我不限于 JPA,也可以使用 hibernate 注释。在最坏的情况下,纯 sql 也将适合。可能有一些我还没有找到的现成库。 不必按照我下面描述的想法。

理想情况下,我需要数据库如下所示:

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

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

i18n 是一个应该只包含 3 列的表:idlocaletext。表parent 有一个列text(如果只有一个字段需要i18​​n,否则更多列)包含来自i18n.id 的值。我在 Parent 类中尝试了以下映射:

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

当禁用 DDL 生成并且我自己创建表时,它似乎可以工作,但是当启用 DDL 生成时,它会生成一个不必要的列 i18n.parent_id 并对其进行约束:

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

我怎样才能摆脱这个额外的列?是否可以避免从i18n 表到parent 表的引用?此链接使重用i18n 表变得困难。我要么需要在i18n 表中保存一些鉴别器值,要么在整个数据库中使用 GUID,因为不同表中的 id 会发生冲突。第一个选项意味着大量的样板代码。第二个选项意味着在当前项​​目中有很多工作要做。

我需要一种可重用的方式将 i18n 添加到实体。我的父类看起来大概是这样的。并且会有几个这样的父类具有必须国际化的不同的字段集

@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
}

【问题讨论】:

  • parent_id 字段有什么不必要的?您的元素集合需要某种方式将生成的表中的行与父实体(外键)相关联。也许展示您希望数据库的外观,有人可以帮助您弄清楚如何将实体映射到它。
  • 我添加了所需的数据库描述。我需要一个可重复使用的 i18n 表来处理来自父级的一对多关系。我需要能够使用同一个 i18n 表来存储其他字段的翻译。 Parent.text & i18n.id 应该是将生成的表中的行与父实体相关联的方式。外键约束可能只存在于 parent.text。
  • 你能展示完整的i18n类吗?
  • 没有 i18n 类。 @ElementCollection 不需要它。
  • MapkeyColumn 不是你想象的那样,因为映射键是键值所独有的。您可以使用“区域设置”的映射键,创建一个以区域设置为键的文本映射。如果您的 i18n 表只使用 ID 值 99 而不是添加新的任意“文本”字段,那么问题是什么?这些文本不应该在实体之间共享,是吗?如果要重用它们,您确实应该将它们设为实体,使用 id+locale 作为复合 pk。 JPA 不允许对外键使用部分值,但 Hibernate 和其他提供者可能有办法映射“文本”-> id。

标签: java hibernate jpa internationalization


【解决方案1】:

看起来您正在搜索休眠复合主键。你应该执行这个技巧:

@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
}

不幸的是,我不知道如何将其映射为“区域设置”地图,但认为可以使用 @JoinColumn 注释,或者尝试关注 @Alan Hay 的帖子。

【讨论】:

    【解决方案2】:

    似乎 Hibernate 不提供任何开箱即用的 i18n 支持,因此您可以正确地为此实施自定义解决方案。 我还可以假设您的目标是以最低的成本为现有项目添加本地化支持。

    我可以建议您在 Parenti18n 表上使用 ManyToMany 关系。

    在这种情况下,您可以根据需要完全独立于 Parenti18n 表结构,但是每个 "Parent" 都有额外的连接表,其中包含来自 i18n 和 @ 的 PK 引用对。 987654329@ 表。还可以使用ManyToMany 方法记录表单i18n 可以在不同的"Parent" 表中重复使用。

    所以你的表结构可能是这样的:

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

    【讨论】:

      【解决方案3】:

      在数据库级别,集合实例由拥有该集合的实体的外键区分。此外键称为集合表的一个或多个集合键列。

      所以我想你想为你的提议禁用伪造密钥生成,简单地说你可以这样做

      @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;
          }
      }
      

      那么当你验证生成的 DDL 时,它的伪造密钥并没有应用于 I18N 表,但仍然获得了能力

      @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 是一个简单的测试(Spring Boot 1.4 版本),会在控制台看到输出:

      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
      

      这是H2 db中的表:

      【讨论】:

      • 在您的解决方案 parent 表中只有 id 字段,没有其他字段可以保留指向 i18n 表的链接。似乎在测试中您从分配给text 映射的i18n 表中获取所有值,因此它有效。可能您创建了一个从 i18n.idparent.id 的链接,并且两者都是空值。这不是一个可行的解决方案。
      • @user1603038 你可以查看DDL,parenti18n表中没有任何链接,关系应该是parent.idi18n.id的字面值。跨度>
      【解决方案4】:

      您不是只是使用错误地指定了@JoinColumn

      @JoinColumn(referencedColumnName="id"))
      

      而不是

      @JoinColumn(name="id"))
      

      http://docs.oracle.com/javaee/6/api/javax/persistence/JoinColumn.html#referencedColumnName()

      在 CollectionTable 映射中使用时,引用的列位于 包含集合的实体的表

      所以,本质上您是在(重新)定义父级中的 ID 列,而 Hibernate 使用其默认命名策略在 i18n 中生成 FK 列。

      使用这个:

      @Entity
      @Table(name = "parent")
      public class Parent {
      
          @Id
          private Long id;
      
          @ElementCollection
          @CollectionTable(name = "i18n", foreignKey = @ForeignKey(name = "ii8n_to_parent_fk"), joinColumns = @JoinColumn(name = "id"))
          @MapKeyColumn(name = "locale")
          @Column(name = "text")
          public Map<String, String> text = new HashMap<>();
      
      }
      

      生成以下内容:

      19:29:08.890 [main] DEBUG jdbc.sqlonly -  org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:51)
      3. create table i18n (id bigint not null, text varchar(255), locale varchar(255) not null, primary 
      key (id, locale)) 
      
      19:29:09.464 [main] DEBUG jdbc.sqlonly -  org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:51)
      3. alter table i18n add constraint ii8n_to_parent_fk foreign key (id) references parent 
      

      【讨论】:

      • 外键 (id) 引用父级 - 这意味着 i18n.id 必须仅包含 parent.id 值,但这不是我需要的。我需要 parent.text 上的外键以仅包含 i18n.id 值。
      • 您能否发布您的完整实体类 Parent 以解释您如何确切看到它的工作原理?您提供了 250 赏金和零答案。这告诉你什么?
      • 我添加了一个父类的示例并稍微更新了问题,但是我需要一个不依赖于父类结构的通用解决方案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-13
      • 2012-09-30
      • 2011-09-29
      相关资源
      最近更新 更多