Структура базы данных JPA для интернационализации

Я пытаюсь получить реализацию JPA простого подхода к интернационализации. Я хочу иметь таблицу переведенных строк, которую я могу ссылаться в нескольких полях в нескольких таблицах. Таким образом, все текстовые вхождения во всех таблицах будут заменены ссылкой на таблицу переведенных строк. В сочетании с идентификатором языка это даст уникальную строку в таблице переведенных строк для этого конкретного поля. Например, рассмотрим схему, которая имеет сущности курса и модуль следующим образом: -

Курс int course_id, int name, int description

Модуль int module_id, int name

Курс. имя, курс .description и module.name все ссылаются на поле id таблицы переведенных строк: -

TranslatedString int id, Строковый язык, Содержимое строки

Это все кажется достаточно простым. Я получаю одну таблицу для всех строк, которые могут быть интернационализированы, и эта таблица используется во всех других таблицах.

Как я могу это сделать в JPA, используя eclipselink 2.4?

Я посмотрел на встроенный ElementCollection, ala this... JPA 2.0: сопоставление карты - это не совсем то, что я 'm after cos, похоже, что он привязывает таблицу переведенных строк к pk принадлежащей ему таблицы. Это означает, что я могу иметь только одно переводимое поле строки для сущности (если я не добавлю новые столбцы соединения в таблицу переводимых строк, которая победит точку, ее противоположность тому, что я пытаюсь сделать). Я также не понимаю, как это будет работать через entites, предположительно, идентификатор каждого объекта должен будет использовать широкую последовательность базы данных, чтобы обеспечить уникальность таблицы переводимых строк.

Кстати, я попробовал пример, как это изложено в этой ссылке, и это не сработало для меня - как только у объекта была добавлена ​​карта локализованной строки, ее сохранение заставило клиентскую сторону бомбить, но на сервере не было очевидной ошибки стороне и ничего не сохранилось в БД: S

Я был вокруг домов на этом около 9 часов до сих пор, я посмотрел на эту интернационализацию с Hibernate, которая, похоже, пытается сделать то же самое, что и ссылка выше (без определения таблиц трудно понять, что он достиг). Любая помощь будет с благодарностью достигнута в этот момент...

Изменить 1 - re AMS anwser ниже, я не уверен, что действительно решает проблему. В своем примере он оставляет сохранение текста описания в каком-то другом процессе. Идея такого подхода заключается в том, что объект entity принимает текст и локаль, и это (как-то!) Заканчивается в таблице переводимых строк. В первой ссылке, которую я дал, парень пытается сделать это, используя встроенную карту, которую я считаю правильным. У его пути есть две проблемы: одна, похоже, не работает! и два, если это сработало, оно хранит FK во встроенной таблице, а не наоборот. Думаю, я не могу заставить его работать, поэтому я не вижу точно, как он сохраняется. Я подозреваю, что правильный подход заканчивается ссылкой на карту вместо каждого текста, который нуждается в переводе (карта - это локаль- > контент), но я не вижу, как это сделать таким образом, чтобы допускать несколько карт в одном объекте (без соответствующих столбцов в таблице переводимых строк)...

Ответы

Ответ 1

Хорошо, я думаю, что у меня есть. Похоже, что упрощенная версия первой ссылки в моем вопросе будет работать, просто используя отношение ManyToOne к локализованной сущности (с другим joinColumn для каждого текстового элемента в вашей основной сущности) и простой ElementCollection для Карты в пределах этого Локализованного объекта, Я кодировал несколько иной пример, чем мой вопрос, только с одним объектом (категория), имеющим два текстовых элемента, которым требуется несколько записей для каждой локали (имя и описание).

Обратите внимание, что это было сделано против Eclipselink 2.4, идущего в MySQL.

Две заметки об этом подходе - как вы можете видеть в первой ссылке, используя ElementCollection, создается отдельная таблица, которая должна быть создана, что приводит к двум таблицам для переводимых строк: один только содержит идентификатор (Locaised), который является FK в основном (Localized_strings), который содержит всю информацию о карте. Имя Localized_strings - это имя автоматически/по умолчанию - вы можете использовать другое с аннотацией @CollectionTable. В целом, это не идеально с точки зрения БД, но не с конца света.

Во-вторых, по крайней мере, для моей комбинации Eclipselink и MySQL, сохраняющейся в одной (автоматически генерируемой) таблице столбцов, возникает ошибка:( Так что я добавил в значение dummy column wa default в сущности, это чисто для решения этой проблемы.

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)

A Main, чтобы проверить его...

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, так и связанные записи Locaised/Localised_strings.

Надеюсь, что все это поможет кому-то в будущем.

Ответ 2

(Я Хенно, который ответил на блог hwellman.) Мой первоначальный подход был очень похож на ваш подход, и он выполняет эту работу. Он отвечает требованию, чтобы любое поле из любого объекта могло ссылаться на локализованную карту строк с общей таблицей базы данных, которая не должна ссылаться на другие более конкретные таблицы. Действительно, я также использую его для нескольких полей в объекте Product (имя, описание, подробности). У меня также была "проблема", что JPA создала таблицу с только столбцом первичного ключа и таблицей для значений, которые ссылались на этот идентификатор. С 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 немного более элегантно.

Существует проблема, о которой вы должны знать: разрешите ли вы делиться локализованным с несколькими объектами и как вы предотвращаете осиротевшие локализованные сущности при удалении объекта-владельца? Просто использовать каскад все не достаточно. Я решил как можно больше локализовать как "объект ценности" и не разрешать его совместное использование с другими объектами, так что нам не нужно думать о нескольких ссылках на один и тот же локализованный, и мы можем безопасно использовать удаление сирот. Поэтому мои локализованные поля отображаются следующим образом:

@OneToOne(cascade = ALL, orphanRemoval = true)

В зависимости от моего варианта использования я также использую fetch = EAGER/LAZY и optional = false или true. При использовании опции optional = false я использую @JoinColumn (nullable = false), поэтому OpenJPA генерирует в столбце объединения не нулевое ограничение.

Всякий раз, когда мне нужно скопировать Локализованный в другой объект, я не использую одну и ту же ссылку, но я создаю новый экземпляр Localized с тем же содержимым и без идентификатора. В противном случае вам может быть сложно отладить проблемы, в которых изменение не выполняется. Вы все еще используете экземпляр с несколькими объектами, и вы можете получить удивительные ошибки, когда изменение локализованной строки может изменить другую строку в другом объекте.

До сих пор так хорошо, однако на практике я обнаружил, что 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 имеет похожие функции, которые могут работать или не работать.

Из отчаяния, чтобы решить эту проблему производительности, я действительно должен был согласиться с тем, что мне не нравится дизайн: я просто добавил поле String для каждого языка, который должен был поддерживать Локализованный. Для нас это возможно, потому что в настоящее время нам нужно поддерживать только несколько языков. Это привело только к одной (денормализованной) локализованной таблице. JPA может затем эффективно присоединяться к таблице Localized в запросах, но это не будет хорошо масштабироваться для многих языков и не поддерживает произвольное количество языков. Для обеспечения работоспособности я сохранил внешний интерфейс Localized одинаково и только изменил реализацию с карты на поле для каждого языка, чтобы мы могли легко вернуться в будущем.

Ответ 3

Вот один из способов сделать это.

Загрузите все переведенные строки из базы данных в кеш, позвонив в MessageCache, у него будет метод, называемый public String getMesssage (int id, int languageCode). Вы можете использовать неизменяемые коллекции google guava для хранения этого в кеше памяти. Вы также можете использовать Guava LoadCache для хранения кэша, если вы хотите загрузить их по запросу. Если у вас есть такой кеш, вы можете написать такой код.

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

Основная проблема, которую я вижу в этом подходе, заключается в том, что вам нужно знать локаль, на которую вы ссылаетесь в сущности, я бы предположил, что задача выбора правильного языка для описания должна выполняться не в сущности, а в абстракция более высокого уровня, например, Дао или Служба.