多语言数据库,默认回退

Posted

技术标签:

【中文标题】多语言数据库,默认回退【英文标题】:Multi language database, with default fallback 【发布时间】:2015-01-02 02:46:40 【问题描述】:

我知道,我有一个问题已被广泛讨论,但在我看来,还有一个方面需要澄清。

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

所以我决定使用一个包含我的项目 ID 的主表和另一个包含每个项目的翻译的表,例如

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】:

一些注意事项:

我的回答更多的是对my answer to this question 的补充,您在其中添加了一条评论,然后导致了这个问题 在我的回答中,我使用的是 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-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;

为了加载这些数据,我使用了不同的查询(或存储过程)。例如。要以 @Language 语言加载 ID 为 @Id 的产品,我将使用以下查询:

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

如果存在该语言的翻译,这将返回所请求语言的产品标题和描述。如果不存在翻译,则返回默认语言的标题和描述。

【讨论】:

太棒了!我将不得不深入研究 Java 和其他项目 API 以找到如何做到这一点,但是由于您的答案非常完整并且您提供了实际的代码和查询,我会将这个问题标记为已回答!非常感谢 M4N! 但我们仍然可以在TitleDescription 中获得NULL。如果在Products 表中创建NOT NULLDefaultTitleDefaultDescription 怎么办?所以,在查询中:ISNULL(tr.Title, p.DefaultTitle) Title【参考方案2】:

对所有表的所有可翻译字段使用公共共享表

在上述方法中,转换表是父表的扩展。因此 ProductTranslation 具有 Product 的所有可翻译字段。这是一种简洁快捷的方法,也是一种不错的方法。

但是有一个缺点(不确定是否可以称为缺点)。如果更多表需要可翻译字段,则需要许多新表。根据我的经验,我们采取了不同的方法。我们创建了一个通用翻译表和一个链接表,将翻译链接到父表的可翻译字段。

所以我将使用前面的 Product 示例,它有两个字段 title 和 description 可以翻译来解释我们的方法。还要考虑另一个表 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

此查询基于 Product 的各个字段没有默认翻译的假设

类似的查询可用于从 ProductCategory 中获取记录

【讨论】:

是的,我也考虑过为所有翻译提供一个通用表,但正如您所说的,拥有多个表意味着数据分散在数据库中。不过,我完全不介意这一点,相反,我认为将不属于彼此的事物分开是一种“好习惯”。无论如何也感谢您的回答,我看到这种方法确实被使用并且值得一试。我还得抽时间好好研究一下,可惜了 我认为这种方法从外观上看是最漂亮的一种,但实际上巨大的负载只会在一张桌子上,这可能会导致系统出现瓶颈性能问题。 我不是真正的数据库专家,所以我的意见可能不相关,但这种方法不存在两个问题:1)据我所知,左连接总是比内连接慢,并且应该减少。这种方法总是需要多个连接,具体取决于翻译字段的数量。使用另一种方法(多个转换表)只需要一个左连接。 2) 数据库级别不保证引用完整性:从 Products 中删除不会自动删除 Translations 中的相应条目,这会导致未使用的数据垃圾。 被低估的答案,imo。创建翻译文件非常方便,因为您可以从一个表中转储而不是具有数十个数据库表的解决方案。它还提供了更多功能,可以自动将非多语言平台转换为多语言。

以上是关于多语言数据库,默认回退的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET MVC 多语言实现——URL路由

NSIS多语言

多语言网站的用户体验

htaccess 带有子目录的多语言站点,默认为 301

Android的应用多语言开发

纸壳CMS可视化建站系统搭建多语言网站