База данных с несколькими языками, с отступлением по умолчанию

У меня есть вопрос, который, я знаю, широко обсуждался, но, на мой взгляд, есть один аспект, который все еще нуждается в разъяснении.

Я создаю веб-приложение с многоязычной базой данных, я уже нашел некоторые статьи о хороших практиках (например, this) и ответы здесь в переполнении стека, например .

Итак, я решил использовать основную таблицу с идентификаторами моих элементов и другую таблицу с переводом для каждого элемента, скажем, например

Content
ContentTranslation

или

Category
CategoryTranslation

и т.д.

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

При большом количестве объектов и переводов время ответа сервера может возрасти, и даже если пользователь может не заметить, я не хочу этого.

Итак, есть ли хорошая практика для этого случая использования? Например, некоторые конкретные запросы, которые говорят "выберите перевод с языковой версией" это ", но если вы не найдете его, просто получите тот, у которого установлен флаг" по умолчанию "?

Теперь для технологии я использую Spring MVC с Hibernate и JPA (с помощью JPARepository).

Мои объекты расширяют базовый класс Translatable, который я сделал таким образом

@MappedSuperclass
public abstract class Translatable<T extends Translation> extends BaseDTO {

    private static final long serialVersionUID = 562001309781752460L;

    private String title;

    @OneToMany(fetch=FetchType.EAGER, orphanRemoval=true, cascade=CascadeType.ALL)
    private Set<T> translations = new HashSet<T>();

    @Transient private T currentLocale;

    public void addLocale(T translation, boolean edit) {
        if (!edit)
            getTranslations().add(translation);
    }

    public void remLocale(String locale) {
        T tr = null;
        for (T candidate: getTranslations()) {
            if (candidate.getLocale().equals(locale))
                tr = candidate;
        }

        getTranslations().remove(tr);
    }

    public T getLocaleFromString(String locale) {
        if (locale == null)
            return null;
        for (T trans: translations) {
            if (trans.getLocale().equals(locale))
                return trans;
        }
        return null;
    }

    public T getDefaultLocale() {
        for (T tr: translations) {
            if (tr.isDefaultLocale())
                return tr;
        }
        return null;
    }

    public Set<T> getTranslations() {
        return translations;
    }

    public void setTranslations(Set<T> translations) {
        this.translations = translations;
    }

    public T getCurrentLocale() {
        return currentLocale;
    }

    public void setCurrentLocale(T currentLocale) {
        this.currentLocale = currentLocale;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

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

Надеюсь, я был ясен и не беспорядочен, но если вам нужна дополнительная информация, я буду рад рассказать вам больше.

Ответ 1

Некоторые примечания:

  • мой ответ скорее является дополнением к моему ответу на этот вопрос, где вы добавили комментарий, который затем привел к этому вопросу.
  • В моем ответе я использую С# и MS SQL Server (и я не буду указывать какой-либо код OR-mapping)

В моих приложениях я использую два разных подхода для загрузки многоязычных данных в зависимости от варианта использования:

Администрирование /CRUD

В случае, когда пользователь вводит данные или редактирует существующие данные (например, продукт с его переводами), я использую тот же подход, который был указан выше в вашем вопросе, например:

public class Product
{
    public int ID {get; set;}
    public string SKU {get; set;}
    public IList<ProductTranslation> Translations {get; set;}
}
public class ProductTranslation
{
    public string Language {get; set;}
    public bool IsDefaultLanguage {get; set;}
    public string Title {get; set;}
    public string Description {get; set;}
}

т.е. Я позволю OR-mapper загружать экземпляр продукта с прикрепленными их переводами. Затем я повторяю переводы и выбираю нужные.

Лицевая сторона/только для чтения

В этом случае, который является главным образом интерфейсным кодом, где я обычно просто показываю информацию пользователю (желательно на языке пользователя), я использую другой подход:

Прежде всего, я использую другую модель данных, которая не поддерживает/не знает понятие нескольких переводов. Вместо этого это просто представление продукта на "наилучшем" языке для текущего пользователя:

public class Product
{
    public int ID {get; set;}
    public string SKU {get; set;}

    // language-specific properties
    public string Title {get; set;}
    public string Description {get; set;}
}

Чтобы загрузить эти данные, я использую разные запросы (или хранимые процедуры). Например. для загрузки продукта с идентификатором @Id на языке @Language, я бы использовал следующий запрос:

SELECT
    p.ID,
    p.SKU,
    -- get title, description from the requested translation,
    -- or fall back to the default if not found:
    ISNULL(tr.Title, def.Title) Title,
    ISNULL(tr.Description, def.Description) Description
  FROM Products p
  -- join requested translation, if available:
  LEFT OUTER JOIN ProductTranslations tr
    ON p.ID = tr.ProductId AND tr.Language = @Language
  -- join default language of the product:
  LEFT OUTER JOIN ProductTranslations def
    ON p.ID = def.ProductId AND def.IsDefaultLanguage = 1
  WHERE p.ID = @Id

Это возвращает заголовок и описание продукта на запрошенном языке, если существует перевод для этого языка. Если перевод не существует, название и описание с языка по умолчанию будут возвращены.

Ответ 2

Использование общей общей таблицы для всех переводимых полей всех таблиц

В приведенном выше подходе таблица переводов является расширением родительской таблицы. Следовательно, ProductTranslation имеет все переводимые поля Product. Это аккуратный и быстрый подход и приятный.

Но есть один недостаток (не уверен, что его можно назвать недостатком). Если для многих таблиц требуются поля, способные переводить, требуется много новых таблиц. По моему опыту мы использовали другой подход. Мы создали общую таблицу для перевода и таблицу ссылок, чтобы связать переводы с полями, соответствующими переводу родительской таблицы.

Итак, я собираюсь использовать предыдущий пример Product, в котором есть два названия и описания полей, которые могут быть переведены, чтобы объяснить наш подход. Также рассмотрите другую таблицу ProductCategory с названием и описанием полей, которые также требуют переводов.

Product
(
   ID: Integer
   SKU: String
   titleID: Integer // ID of LocalizableText record corresponding title
   descriptionID: Integer // ID of LocalizableText record corresponding description
)

ProductCategory
(
   ID: Integer
   nameID: Integer // ID of LocalizableText record corresponding name
   descriptionID: Integer // ID of LocalizableText record corresponding description
)

LocalizableText // This is nothing but a link table
{
    ID: Integer
}

Translations //This is where all translations are stored.
{
    ID: Integer
    localizableTextID: Integer
    language: String
    text: String
}

Чтобы загрузить эти данные, я использую разные запросы (измененный выше). Например. для загрузки продукта с идентификатором @Id на языке @Language, я бы использовал следующий запрос

SELECT
    p.ID,
    p.SKU,
    -- get title, description from the requested translation,
    -- or fall back to the default if not found:
    Title.text Title,
    description.text Description
  FROM Products p
  -- join requested translation for title, if available:
  LEFT OUTER JOIN Translations title
    ON p.titleID = title.localizableTextID
       AND title.Language = @Language
  -- join requested translation for description, if available:
  LEFT OUTER JOIN Translations description
    ON p.descriptionID = description.localizableTextID
       AND description.Language = @Language
  WHERE p.ID = @Id

Этот запрос основан на предположении, что отдельные поля продукта не имеют перевода по умолчанию

Аналогичный запрос может использоваться для извлечения записей из ProductCategory