JPA数据库结构的国际化

时间:2012-11-17 00:11:35

标签: java jpa

我试图让JPA实现一种简单的国际化方法。我想要一个已翻译的字符串表,我可以在多个表的多个字段中引用它。因此,所有表中的所有文本出现都将被对已翻译字符串表的引用所取代。结合语言ID,这将在该特定字段的翻译字符串表中给出唯一的行。例如,考虑具有实体课程和模块的模式,如下所示: -

课程 int course_id, int name, int description

模块 int module_id, int name

course.name,course.description和module.name都引用了翻译的字符串表的id字段: -

TranslatedString int id, String lang, 字符串内容

这一切看起来都很简单。我为所有可以国际化的字符串得到一个表,并且该表用于所有其他表。

如何使用eclipselink 2.4在JPA中执行此操作?

我查看了嵌入式ElementCollection,ala this ... JPA 2.0: Mapping a Map - 它不完全是我所追求的它看起来它将翻译的字符串表与拥有表的pk相关联。这意味着每个实体只能有一个可翻译的字符串字段(除非我将新的连接列添加到可翻译的字符串表中,这会破坏这一点,这与我想要做的相反)。我也不清楚它如何在entites中工作,可能每个实体的id必须使用数据库范围的序列来确保可翻译字符串表的唯一性。

顺便说一下,我尝试了那个链接中列出的例子并且它对我不起作用 - 一旦实体添加了localizedString映射,持久化导致客户端炸弹但服务器上没有明显错误DB:S

中没有任何内容

到目前为止,我在这个房子周围大约9个小时,我看过这个Internationalization with Hibernate似乎试图做与上面的链接相同的事情(没有表定义很难看到他取得了什么)。在这一点上,任何帮助都会感激不尽......

编辑1 - 下面是AMS anwser,我不确定是否真的解决了这个问题。在他的例子中,它将描述文本的存储留给了其他一些进程。这种方法的想法是实体对象采用文本和语言环境,这(以某种方式!)最终在可翻译的字符串表中。在我给出的第一个链接中,这个人试图通过使用嵌入式地图来做到这一点,我觉得这是正确的方法。他的方式有两个问题 - 一个似乎不起作用!它有两个,如果它确实有效,它将FK存储在嵌入式表中而不是相反的方式(我认为,我无法运行它,所以我无法确切地看到它是如何持续存在的)。我怀疑正确的方法最终会有一个地图参考代替每个需要翻译的文本(地图是locale->内容),但是我无法看到如何以一种允许多个地图的形式实体(在可翻译字符串表中没有相应的多个列)...

4 个答案:

答案 0 :(得分:9)

(我是Henno谁回复了hwellman的博客。)我最初的方法与你的方法非常相似,它完成了这项工作。它满足了任何实体的任何字段都可以引用具有通用数据库表的本地化字符串映射的要求,该数据库表不必引用其他更具体的表。实际上,我也将它用于我们的产品实体中的多个字段(名称,描述,详细信息)。我还遇到了“问题”,即JPA生成的表只包含主键列和引用此id的值的表。使用OpenJPA,我不需要虚拟列:

public class StringI18N {

    @OneToMany(mappedBy = "parent", cascade = ALL, fetch = EAGER, orphanRemoval = true)
    @MapKey(name = "locale")
    private Map<Locale, StringI18NSingleValue> strings = new HashMap<Locale, StringI18NSingleValue();
...

OpenJPA只是将Locale存储为String。因为我们不需要额外的实体StringI18NSingleValue所以我认为使用@ElementCollection的映射更优雅。

但是您必须注意一个问题:您是否允许与多个实体共享Localized,以及如何在删除拥有实体时阻止孤立的本地化实体?仅仅使用级联是不够的。我决定尽可能地将Localized视为“值对象”,并且不允许它与其他实体共享,这样我们就不必考虑对同一个Localized的多个引用,我们可以安全地使用孤立删除。所以我的Localized字段映射如下:

@OneToOne(cascade = ALL, orphanRemoval = true)

根据我的用例,我还使用fetch = EAGER / LAZY和optional = false或true。当使用optional = false时,我使用@JoinColumn(nullable = false),因此OpenJPA在连接列上生成一个非空约束。

每当我确实需要将Localized复制到另一个实体时,我不会使用相同的引用,但是我创建了一个具有相同内容且尚无id的新Localized实例。否则你可能很难调试changin的问题如果你不这样做,你仍然与多个实体共享一个实例,你可能会遇到令人惊讶的错误,更改Localized String可以改变另一个实体的另一个String。

到目前为止一直很好,但实际上我发现OpenJPA在选择包含一个或多个Localized Strings的实体时有N + 1个选择问题。它无法有效地获取元素集合(我将其报告为https://issues.apache.org/jira/browse/OPENJPA-1920)。可以通过使用Map&lt; Locale,StringI18NSingleValue&gt;来解决该问题。然而,OpenJPA也无法有效地获取A 1..1 B 1 .. * C形式的结构,这也是这里发生的事情(我将其报告为https://issues.apache.org/jira/browse/OPENJPA-2296)。这会严重影响您的应用程序的性能。

其他JPA提供商可能有类似的N + 1选择问题。如果您关注获取类别的性能,我会检查用于获取Category的查询数量是否取决于实体数量。我知道使用Hibernate可以强制批量提取或子选择来解决这些问题。我也知道EclipseLink具有类似的功能,可能会也可能不起作用。

出于对解决这个性能问题的绝望,我实际上不得不接受一个我不喜欢的设计生活:我只是为每个必须支持Localized的语言添加了一个String字段。对我们来说这是可能的,因为我们目前只需要支持几种语言。这导致只有一个(非规范化)本地化表。然后,JPA可以有效地在查询中加入Localized表,但这对于许多语言来说不能很好地扩展,并且不支持任意数量的语言。为了可维护性,我保持Localized的外部接口相同,只是将实现从Map更改为每个字段的字段,以便我们以后可以轻松切换回来。

答案 1 :(得分:6)

好的,我想我拥有它。它看起来像我的问题中的第一个链接的简化版本将工作,只是使用ManyToOne关系到本地化实体(主实体中的每个文本元素具有不同的joinColumn)和该本地化实体中的Map的简单ElementCollection 。我编写了一个与我的问题略有不同的例子,只有一个实体(类别),有两个文本元素,每个区域设置需要多个条目(名称和描述)。

请注意,这是针对Eclipselink 2.4进入MySQL的。

关于这种方法的两个注意事项 - 正如您在第一个链接中看到的那样,使用ElementCollection强制创建一个单独的表,这会导致两个表用于可翻译的字符串 - 一个只保存ID(Locaised),即FK在主要的(Localised_strings)中保存所有Map信息。名称Localised_strings是自动/默认名称 - 您可以使用另一个带@CollectionTable注释的名称。总的来说,从数据库的角度来看,这并不理想,但不是世界末日。

其次,至少对于我的Eclipselink和MySQL的组合,持久化到单个(自动生成的)列表会产生错误:(所以我在实体中添加了虚拟列wa默认值,这是纯粹是为了克服这个问题。

import java.io.Serializable;
import java.lang.Long;
import java.lang.String;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.*;


@Entity

public class Category implements Serializable {

@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@ManyToOne(cascade=CascadeType.ALL)
@JoinColumn(name="NAME_ID")
private Localised nameStrings = new Localised();

@ManyToOne(cascade=CascadeType.ALL)
@JoinColumn(name="DESCRIPTION_ID")
private Localised descriptionStrings = new Localised();

private static final long serialVersionUID = 1L;

public Category() {

    super();
}  

public Category(String locale, String name, String description){
    this.nameStrings.addString(locale, name);
    this.descriptionStrings.addString(locale, description);
}
public Long getId() {
    return this.id;
}

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

public String getName(String locale) {
    return this.nameStrings.getString(locale);
}

public void setName(String locale, String name) {
    this.nameStrings.addString(locale, name);
}
public String getDescription(String locale) {
    return this.descriptionStrings.getString(locale);
}

public void setDescription(String locale, String description) {
    this.descriptionStrings.addString(locale, description);
}

}




import java.util.HashMap;
import java.util.Map;

import javax.persistence.ElementCollection;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Localised {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int id;
    private int dummy = 0;
    @ElementCollection
    private Map<String,String> strings = new HashMap<String, String>();

    //private String locale;    
    //private String text;

    public Localised() {}

    public Localised(Map<String, String> map) {
        this.strings = map;
    }

    public void addString(String locale, String text) {
        strings.put(locale, text);
    }

    public String getString(String locale) {
        String returnValue = strings.get(locale);
        return (returnValue != null ? returnValue : null);
    }

}

所以这些生成表如下: -

CREATE TABLE LOCALISED (ID INTEGER AUTO_INCREMENT NOT NULL, DUMMY INTEGER, PRIMARY KEY (ID))
CREATE TABLE CATEGORY (ID BIGINT AUTO_INCREMENT NOT NULL, DESCRIPTION_ID INTEGER, NAME_ID INTEGER, PRIMARY KEY (ID))
CREATE TABLE Localised_STRINGS (Localised_ID INTEGER, STRINGS VARCHAR(255), STRINGS_KEY VARCHAR(255))
ALTER TABLE CATEGORY ADD CONSTRAINT FK_CATEGORY_DESCRIPTION_ID FOREIGN KEY (DESCRIPTION_ID) REFERENCES LOCALISED (ID)
ALTER TABLE CATEGORY ADD CONSTRAINT FK_CATEGORY_NAME_ID FOREIGN KEY (NAME_ID) REFERENCES LOCALISED (ID)
ALTER TABLE Localised_STRINGS ADD CONSTRAINT FK_Localised_STRINGS_Localised_ID FOREIGN KEY (Localised_ID) REFERENCES LOCALISED (ID)

主要测试它......

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;

public class Main {
  static EntityManagerFactory emf = Persistence.createEntityManagerFactory("javaNetPU");
  static EntityManager em = emf.createEntityManager();

  public static void main(String[] a) throws Exception {
    em.getTransaction().begin();


    Category category = new Category();

    em.persist(category);

    category.setName("EN", "Business");
    category.setDescription("EN", "This is the business category");


    category.setName("FR", "La Business");
    category.setDescription("FR", "Ici es la Business");

    em.flush();

    System.out.println(category.getDescription("EN"));
    System.out.println(category.getName("FR"));


    Category c2 = new Category();
    em.persist(c2);

    c2.setDescription("EN", "Second Description");
    c2.setName("EN", "Second Name");

    c2.setDescription("DE", "Zwei  Description");
    c2.setName("DE", "Zwei  Name");

    em.flush();


    //em.remove(category);


    em.getTransaction().commit();
    em.close();
    emf.close();

  }
}

这会产生输出: -

This is the business category
La Business

和以下表格条目: -

Category
"ID"    "DESCRIPTION_ID"    "NAME_ID"
"1"         "1"                 "2"
"2"         "3"                 "4"

Localised
"ID"    "DUMMY"
"1"         "0"
"2"         "0"
"3"         "0"
"4"         "0"

Localised_strings

"Localised_ID"  "STRINGS"                        "STRINGS_KEY"
"1"                 "Ici es la Business"                 "FR"
"1"                 "This is the business category"      "EN"
"2"                 "La Business"                        "FR"
"2"                 "Business"                       "EN"
"3"                 "Second Description"                 "EN"
"3"                 "Zwei  Description"              "DE"
"4"                 "Second Name"                        "EN"
"4"                 "Zwei  Name"                         "DE"

取消注释em.remove会正确删除类别及其关联的Locaised / Localised_strings条目。

希望将来所有人都能帮助。

答案 2 :(得分:1)

我知道有些晚了,但是我实现了以下方法:

 @Entity
 public class LocalizedString extends Item implements Localizable<String>
 {
     @Column(name = "en")
     protected String en;

     @Column(name = "en_GB")
     protected String en_GB;

     @Column(name = "de")
     protected String de;

     @Column(name = "de_DE")
     protected String de_DE;

     @Column(name = "fr")
     protected String fr;

     @Column(name = "fr_FR")
     protected String fr_FR;

     @Column(name = "es")
     protected String es;

     @Column(name = "es_ES")
     protected String es_ES;

     @Column(name = "it")
     protected String it;

     @Column(name = "it_IT")
     protected String it_IT;

     @Column(name = "ja")
     protected String ja;

     @Column(name = "ja_JP")
     protected String ja_JP;
 }

该实体没有设置者和获取者!相反,Localizable接口定义了常见的get / set方法:

public class Localizable<T> {
    private final KeyValueMapping<Locale, T> values = new KeyValueMapping<>();

    private T defaultValue = null;

    /**
     * Generates a {@link Localizable} that only holds one value - for all locales.
     * This value overrides all localalized values when using
     * {@link Localizable#toString()} or {@link Localizable#get()}.
     */
    public static <T> Localizable<T> of(T value) {
        return new Localizable<>(value);
    }

    public static <T> Localizable<T> of(Locale locale, T value) {
        return new Localizable<>(locale, value);
    }

    private Localizable(T value) {
        this.defaultValue = value;
    }

    private Localizable(Locale locale, T value) {
        this.values.put(locale, value);
    }

    public Localizable() {
    }

    public void set(Locale locale, T value) {
        values.put(locale, value);
    }

    /**
     * Returns the value associated with the default locale
     * ({@link Locale#getDefault()}) or the default value, if it is set.
     */
    public T get() {
        return defaultValue != null ? defaultValue : values.get(Locale.getDefault());
    }

    public T get(Locale locale) {
        return values.get(locale);
    }

    /**
     * Returns the toString of the value for the default locale
     * ({@link Locale#getDefault()}).
     */
    @Override
    public String toString() {
        if (defaultValue != null) {
            return defaultValue.toString();
        }

        return toString(Locale.getDefault());
    }

    /**
     * Returns toString of the localized value.
     * 
     * @return null if there is no localized.
     */
    public String toString(Locale locale) {
        return values.transformValue(locale, v -> v.toString());
    }

    public Map<Locale, T> getValues() {
        return Collections.unmodifiableMap(values);
    }

    public T getDefaultValue() {
        return defaultValue;
    }

    public void setDefaultValue(T defaultValue) {
        this.defaultValue = defaultValue;
    }

}

此方法的巨大优势是您只有一个可本地化的实体,并且本地化的值存储在列中(而不是每个本地化都有一个实体)。

答案 3 :(得分:-1)

这是一种方法。

将所有已翻译的字符串从数据库加载到缓存中,让我们称之为MessagesCache,它将有一个名为public String getMesssage(int id,int languageCode)的方法。您可以使用google guava immutable集合将其存储在内存缓存中。如果要按需加载它们,还可以使用Guava LoadingCache存储值的缓存。如果你有这样的缓存,你可以像这样编写代码。

@Entity 
public Course {
    @Column("description_id")
    private int description;

    public String getDescription(int languageCode)
    { 
        return this.messagesCache(description, languageCode);
    }


    public String setDscription(int descriptionId)
    {
         this.description = descriptionId; 
    }
} 

我看到这个方法的主要问题是你需要知道你在实体中引用的语言环境,我建议选择正确的描述语言的任务不应该在实体中完成,而是在更高级别的抽象,例如Dao或Service。