多语言数据库,具有默认回退

2022-09-01 16:34:41

我知道,我有一个问题已经被广泛讨论过,但我认为,有一个方面仍然需要澄清。

我正在创建一个具有多语言数据库的Web应用程序,我已经找到了一些最佳实践文章(例如这个),并在堆栈溢出中给出了这样的答案。

因此,我决定使用一个包含我项目的ID的主表和另一个包含每个项目的翻译的表,例如,让我们说

Content
ContentTranslation

Category
CategoryTranslation

等等。

现在我在做什么?我只是从数据库中获取包含所有翻译的项目,然后我迭代每个项目以根据当前用户的本地语言查找正确的翻译,如果我找到正确的本地,我将设置到翻译的主要对象中,以便页面呈现,否则我只得到标记为“默认”翻译的翻译。

但是,对于大量的对象和翻译,服务器响应时间可能会增加,即使用户可能没有注意到,我也不希望这样。

那么,对于这个用例,是否有任何好的做法呢?例如,一些特定的查询说“选择带有区域设置”it“的翻译,但如果您没有找到它,只需获取设置了”默认“标志的翻译?

现在,对于这项技术,我正在使用带有Hibernate和JPA的Spring MVC(通过JPARepository)。

我的对象都扩展了我以这种方式创建的基本可翻译类

@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

前期的一些注意事项:

  • 我的答案更像是我对这个问题的回答的补充,你添加了一个评论,然后导致了这个问题。
  • 在我的答案中,我正在使用C#和MS SQL Server(我将省略任何OR映射特定的代码)

在我的应用程序中,我使用两种不同的方法来加载多语言数据,具体取决于用例:

管理/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 映射器加载产品实例,并附加其所有翻译。然后,我循环访问翻译并选择所需的翻译。

前端/只读

在这种情况下,主要是前端代码,我通常只向用户显示信息(最好是用用户的语言),我使用不同的方法:

首先,我使用的是一个不同的数据模型,它不支持/不知道多个翻译的概念。相反,它只是以当前用户的“最佳”语言表示产品:

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的产品,我将使用以下查询:@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
}

为了加载此数据,我使用了不同的查询(修改了上面的查询)。例如,要以@Language的语言加载ID@Id的产品,我将使用以下查询

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

此查询基于以下假设:产品的各个字段没有默认翻译

类似的查询可用于从产品类别中提取记录


推荐