15

我正在尝试实现一种简单的国际化方法的 JPA 实现。我想要一个翻译字符串表,我可以在多个表的多个字段中引用它。因此,所有表中出现的所有文本都将替换为对已翻译字符串表的引用。结合语言 ID,这将在已翻译字符串表中为该特定字段提供唯一行。例如,考虑具有实体 Course 和 Module 的架构,如下所示:-

课程 int course_id, int name, int description

模块 int module_id, int name

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

TranslatedString int id, String lang, String content

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

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

我已经查看了嵌入式 ElementCollection,这... JPA 2.0:映射地图- 这并不完全是我所追求的,因为它看起来将翻译后的字符串表与拥有表的 pk 相关联。这意味着每个实体只能有一个可翻译的字符串字段(除非我将新的连接列添加到可翻译的字符串表中,这违背了这一点,这与我试图做的相反)。我也不清楚这将如何跨实体工作,大概每个实体的 id 必须使用数据库范围的序列来确保可翻译字符串表的唯一性。

顺便说一句,我尝试了该链接中列出的示例,但它对我不起作用 - 一旦实体添加了本地化字符串映射,坚持它会导致客户端炸弹但服务器端没有明显错误,什么也没有保存在数据库中:S

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

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

4

4 回答 4

9

(我是回复 hwellman 博客的 Henno。)我最初的方法与您的方法非常相似,并且可以完成工作。它满足任何实体的任何字段都可以引用本地化字符串映射的要求,该字符串映射具有通用数据库表,而不必引用其他更具体的表。事实上,我还将它用于我们产品实体中的多个字段(名称、描述、详细信息)。我还有一个“问题”,即 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 存储为字符串。因为我们真的不需要额外的实体 StringI18NSingleValue 所以我认为你使用 @ElementCollection 的映射更优雅一点。

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

@OneToOne(cascade = ALL, orphanRemoval = true)

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

每当我确实需要将 Localized 复制到另一个实体时,我不会使用相同的引用,但我会创建一个新的 Localized 实例,它具有相同的内容但还没有 id。否则,您可能很难调试 changin 如果您不这样做,您仍然与多个实体共享一个实例,并且您可能会遇到令人惊讶的错误,即更改本地化字符串可能会更改另一个实体的另一个字符串。

到目前为止一切顺利,但在实践中,我发现 OpenJPA 在选择包含一个或多个本地化字符串的实体时存在 N+1 选择问题。它不能有效地获取元素集合(我将此报告为https://issues.apache.org/jira/browse/OPENJPA-1920)。这个问题可能通过使用 Map<Locale, StringI18NSingleValue> 来解决。然而,OpenJPA 也不能有效地获取 A 1..1 B 1..* C 形式的结构,这也是这里发生的情况(我将其报告为https://issues.apache.org/jira/browse/OPENJPA-2296)。这会严重影响应用程序的性能。

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

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

于 2012-11-21T09:33:54.567 回答
7

好的,我想我有它。看起来我的问题中第一个链接的简化版本将起作用,只需使用与本地化实体的多对一关系(主实体中的每个文本元素都有不同的 joinColumn)和该本地化实体中的 Map 的简单 ElementCollection . 我编写了一个与我的问题略有不同的示例,只有一个实体(类别),有两个文本元素,每个语言环境需要多个条目(名称和描述)。

请注意,这是针对 Eclipselink 2.4 转到 MySQL 的。

关于这种方法的两个注意事项 - 正如您在第一个链接中看到的那样,使用 ElementCollection 会强制创建一个单独的表,这会导致可翻译字符串的两个表 - 一个只保存 ID(本地化),它是主要的 FK一个(Localised_strings)包含所有地图信息。名称 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 会正确删除 Category 及其关联的 Localised/Localised_strings 条目。

希望对未来的人有所帮助。

于 2012-11-17T18:57:19.007 回答
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;
    }

}

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

于 2018-07-31T18:37:53.687 回答
-1

这是一种方法。

将所有翻译的字符串从数据库加载到缓存中,我们称之为 MessagesCache,它会有一个名为 public String getMesssage(int id, int languageCode) 的方法。您可以使用 google guava 不可变集合将其存储在内存缓存中。如果您想按需加载缓存值,您还可以使用 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。

于 2012-11-17T01:34:50.843 回答