多语言数据库的架构


235

我正在开发一种多语言软件。就应用程序代码而言,可本地化性不是问题。我们可以使用特定于语言的资源,并拥有与之配合使用的各种工具。

但是,定义多语言数据库架构的最佳方法是什么?假设我们有很多表(100个或更多),每个表可以有多个可以本地化的列(大多数nvarchar列应该可以本地化)。例如,其中一个表可能包含产品信息:

CREATE TABLE T_PRODUCT (
  NAME        NVARCHAR(50),
  DESCRIPTION NTEXT,
  PRICE       NUMBER(18, 2)
)

我可以想到三种方法来支持“名称”和“ DESCRIPTION”列中的多语言文本:

  1. 每种语言的单独列

    当我们向系统添加新语言时,我们必须创建其他列来存储翻译后的文本,如下所示:

    CREATE TABLE T_PRODUCT (
      NAME_EN        NVARCHAR(50),
      NAME_DE        NVARCHAR(50),
      NAME_SP        NVARCHAR(50),
      DESCRIPTION_EN NTEXT,
      DESCRIPTION_DE NTEXT,
      DESCRIPTION_SP NTEXT,
      PRICE          NUMBER(18,2)
    )
    
  2. 带有每种语言列的翻译表

    代替存储翻译的文本,仅存储翻译表的外键。翻译表包含每种语言的列。

    CREATE TABLE T_PRODUCT (
      NAME_FK        int,
      DESCRIPTION_FK int,
      PRICE          NUMBER(18, 2)
    )
    
    CREATE TABLE T_TRANSLATION (
      TRANSLATION_ID,
      TEXT_EN NTEXT,
      TEXT_DE NTEXT,
      TEXT_SP NTEXT
    )
    
  3. 每种语言都有行的翻译表

    代替存储翻译的文本,仅存储翻译表的外键。翻译表仅包含一个键,而单独的表则包含每种语言翻译的一行。

    CREATE TABLE T_PRODUCT (
      NAME_FK        int,
      DESCRIPTION_FK int,
      PRICE          NUMBER(18, 2)
    )
    
    CREATE TABLE T_TRANSLATION (
      TRANSLATION_ID
    )
    
    CREATE TABLE T_TRANSLATION_ENTRY (
      TRANSLATION_FK,
      LANGUAGE_FK,
      TRANSLATED_TEXT NTEXT
    )
    
    CREATE TABLE T_TRANSLATION_LANGUAGE (
      LANGUAGE_ID,
      LANGUAGE_CODE CHAR(2)
    )
    

每种解决方案都各有利弊,我想知道您在使用这些方法时的经验,您的建议以及如何设计多语言数据库架构。



3
您可以查看以下链接:gsdesign.ro/blog/multilanguage-database-design-approach尽管阅读评论非常有帮助
Fareed Alnamrouti 2012年

3
LANGUAGE_CODE是自然键,请避免LANGUAGE_ID
Givenkoa

1
我已经看过/使用过2.和3.,不推荐使用它们,您很容易以孤立行结尾。@SunWiKung的设计看起来更好IMO。
Guillaume86

4
我更喜欢SunWuKungs的设计,这恰好是我们已经实现的设计。但是,您需要考虑归类。至少在Sql Server中,每列都有一个归类属性,该属性确定诸如区分大小写,重音字符是否等效以及其他特定于语言的注意事项之类的事情。是否使用特定语言的归类取决于整个应用程序的设计,但是如果弄错了,以后将很难更改。如果需要特定于语言的排序规则,则每种语言都需要一列,而不是每种语言都需要一行。
Elroy Flynn

Answers:


113

您对每个可翻译表都有一个相关的翻译表怎么看?

创建表T_PRODUCT(pr_id int,PRICE NUMBER(18,2))

创建表T_PRODUCT_tr(pr_id INT FK,语言代码varchar,pr_name文本,pr_descr文本)

这样,如果您有多个可翻译列,则只需一个连接即可获取它,因为您没有自动生成translationid,因此将项及其相关翻译一起导入可能会更容易。

不利的一面是,如果您具有复杂的语言回退机制,则可能需要为每个转换表实现该功能-如果您依靠某种存储过程来做到这一点。如果您通过应用程序执行此操作,则可能不会有问题。

让我知道您的想法-我还将就下一个应用程序做出决定。到目前为止,我们已经使用了您的第三种类型。


2
此选项类似于我的选项nr 1,但更好。仍然很难维护,并且需要为新语言创建新表,因此我不愿意实现它。
qbeuek

28
它不需要使用新语言的新表-您只需使用新语言将新行添加到相应的_tr表中即可,如果您创建了新的可翻译表,则只需创建新的_tr表

3
我相信这是一个好方法。其他方法需要大量的左联接,当您联接多个表时,每个表都有翻译,如3层深,每个表都有3个字段,您仅需要3 * 3 9个左联接才能进行翻译..否则3。更容易添加约束等,我相信搜索更合理。
GorillaApe'5

1
T_PRODUCT有100万行时,T_PRODUCT_tr将有200 万行。它将大大降低sql效率吗?
秘银2014年

1
@Mithril不管哪种方式,您都有200万行。至少您不需要使用此方法进行联接。
David D

56

这是一个有趣的问题,所以让我们开始讨论吧。

让我们从方法1的问题开始:
问题:您正在对非规范化以节省速度。
在SQL中(带有hstore的PostGreSQL除外),您不能传递参数语言,并说:

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

因此,您必须这样做:

SELECT 
    Product_UID 
    ,
    CASE @in_language 
        WHEN 'DE' THEN DESCRIPTION_DE 
        WHEN 'SP' THEN DESCRIPTION_SP 
        ELSE DESCRIPTION_EN 
    END AS Text 
FROM T_Products 

这意味着,如果添加新语言,则必须更改所有查询。这自然会导致使用“动态SQL”,因此您不必更改所有查询。

这通常会导致类似这样的情况(并且顺便说一下,它不能用于视图或表值函数中,如果您确实需要过滤报告日期,这确实是一个问题)

CREATE PROCEDURE [dbo].[sp_RPT_DATA_BadExample]
     @in_mandant varchar(3) 
    ,@in_language varchar(2) 
    ,@in_building varchar(36) 
    ,@in_wing varchar(36) 
    ,@in_reportingdate varchar(50) 
AS
BEGIN
    DECLARE @sql varchar(MAX), @reportingdate datetime

    -- Abrunden des Eingabedatums auf 00:00:00 Uhr
    SET @reportingdate = CONVERT( datetime, @in_reportingdate) 
    SET @reportingdate = CAST(FLOOR(CAST(@reportingdate AS float)) AS datetime)
    SET @in_reportingdate = CONVERT(varchar(50), @reportingdate) 

    SET NOCOUNT ON;


    SET @sql='SELECT 
         Building_Nr AS RPT_Building_Number 
        ,Building_Name AS RPT_Building_Name 
        ,FloorType_Lang_' + @in_language + ' AS RPT_FloorType 
        ,Wing_No AS RPT_Wing_Number 
        ,Wing_Name AS RPT_Wing_Name 
        ,Room_No AS RPT_Room_Number 
        ,Room_Name AS RPT_Room_Name 
    FROM V_Whatever 
    WHERE SO_MDT_ID = ''' + @in_mandant + ''' 

    AND 
    ( 
        ''' + @in_reportingdate + ''' BETWEEN CAST(FLOOR(CAST(Room_DateFrom AS float)) AS datetime) AND Room_DateTo 
        OR Room_DateFrom IS NULL 
        OR Room_DateTo IS NULL 
    ) 
    '

    IF @in_building    <> '00000000-0000-0000-0000-000000000000' SET @sql=@sql + 'AND (Building_UID  = ''' + @in_building + ''') '
    IF @in_wing    <> '00000000-0000-0000-0000-000000000000' SET @sql=@sql + 'AND (Wing_UID  = ''' + @in_wing + ''') '

    EXECUTE (@sql) 

END


GO

这是
一个问题:a)日期格式是特定于语言的,因此,如果您未以ISO格式输入(一般的菜园程序员通常不会这样做,并且在即使用户明确指示这样做,用户也肯定不会为您做任何举报)。

B)最显著,你失去任何语法检查。如果<insert name of your "favourite" person here>由于突然改变了机翼的需求而更改了架构,并创建了一个新表,则旧表保留了下来,但参考字段已重命名,那么您不会得到任何警告。在不选择wing参数(==> guid.empty)的情况下运行报表甚至可以运行。但是突然之间,当实际用户实际选择机翼时,==>繁荣这种方法完全破坏了任何一种测试。


方法2:
简而言之:“好主意”(警告-讽刺),让我们结合方法3的缺点(许多条目时速度慢)和方法1
的可怕缺点。 唯一的优点是您保持所有翻译都放在一个表中,因此使维护变得简单。但是,使用方法1和动态SQL存储过程以及包含转换的表(可能是临时表)和目标表的名称(假设您将所有文本字段命名为相同)。


方法3:
所有翻译使用一个表:缺点:您必须在product表中存储要翻译的n个字段的n个外键。因此,您必须对n个字段进行n个联接。当转换表是全局表时,它有许多条目,并且连接变慢。同样,对于n个字段,您始终必须连接T_TRANSLATION表n次。这是相当大的开销。现在,当您必须为每个客户提供定制翻译时,您会怎么做?您必须在另一个表上添加另一个2x n个联接。如果必须联接,比如说10个表,还有2x2xn = 4n个附加联接,那真是一团糟!同样,这种设计使得可以对2个表使用相同的转换。如果我在一个表中更改了项目名称,我真的想同时在另一个表中更改条目吗?

另外,您不能再删除并重新插入表,因为产品表中现在有外键...您当然可以省略设置FK,然后<insert name of your "favourite" person here>可以删除表并重新插入所有带有newid()的条目或通过在插入中指定ID,但将identity-insert设置为OFF),这将(并且将)很快导致数据垃圾(和null引用异常)。


方法4(未列出):将所有语言存储在数据库的XML字段中。例如

-- CREATE TABLE MyTable(myfilename nvarchar(100) NULL, filemeta xml NULL )


;WITH CTE AS 
(
      -- INSERT INTO MyTable(myfilename, filemeta) 
      SELECT 
             'test.mp3' AS myfilename 
            --,CONVERT(XML, N'<?xml version="1.0" encoding="utf-16" standalone="yes"?><body>Hello</body>', 2) 
            --,CONVERT(XML, N'<?xml version="1.0" encoding="utf-16" standalone="yes"?><body><de>Hello</de></body>', 2) 
            ,CONVERT(XML
            , N'<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<lang>
      <de>Deutsch</de>
      <fr>Français</fr>
      <it>Ital&amp;iano</it>
      <en>English</en>
</lang>
            ' 
            , 2 
            ) AS filemeta 
) 

SELECT 
       myfilename
      ,filemeta
      --,filemeta.value('body', 'nvarchar') 
      --, filemeta.value('.', 'nvarchar(MAX)') 

      ,filemeta.value('(/lang//de/node())[1]', 'nvarchar(MAX)') AS DE
      ,filemeta.value('(/lang//fr/node())[1]', 'nvarchar(MAX)') AS FR
      ,filemeta.value('(/lang//it/node())[1]', 'nvarchar(MAX)') AS IT
      ,filemeta.value('(/lang//en/node())[1]', 'nvarchar(MAX)') AS EN
FROM CTE 

然后,您可以通过SQL中的XPath-Query获取值,您可以在其中将字符串变量放入

filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla

您可以像这样更新值:

UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with "&quot;I am a ''value &quot;"')
WHERE id = 1 

在哪里可以替换/lang/de/...'.../' + @in_language + '/...'

有点类似于PostGre hstore,但是由于解析XML的开销(而不是从PG hstore中的关联数组读取条目),它变得太慢了,而且xml编码使它变得太痛苦而无法使用。


方法5(SunWuKung建议,应选择一种方法):每个“产品”表都有一个转换表。这意味着每种语言一行,还有几个“文本”字段,因此在N个字段上只需要一个(左)联接。然后,您可以轻松地在“产品”表中添加默认字段,可以轻松地删除并重新插入翻译表,还可以创建第二个表用于自定义翻译(按需),也可以将其删除并重新插入),您仍然拥有所有外键。

让我们举个例子看看这个作品:

首先,创建表:

CREATE TABLE dbo.T_Languages
(
     Lang_ID int NOT NULL
    ,Lang_NativeName national character varying(200) NULL
    ,Lang_EnglishName national character varying(200) NULL
    ,Lang_ISO_TwoLetterName character varying(10) NULL
    ,CONSTRAINT PK_T_Languages PRIMARY KEY ( Lang_ID )
);

GO




CREATE TABLE dbo.T_Products
(
     PROD_Id int NOT NULL
    ,PROD_InternalName national character varying(255) NULL
    ,CONSTRAINT PK_T_Products PRIMARY KEY ( PROD_Id )
); 

GO



CREATE TABLE dbo.T_Products_i18n
(
     PROD_i18n_PROD_Id int NOT NULL
    ,PROD_i18n_Lang_Id int NOT NULL
    ,PROD_i18n_Text national character varying(200) NULL
    ,CONSTRAINT PK_T_Products_i18n PRIMARY KEY (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id)
);

GO

-- ALTER TABLE dbo.T_Products_i18n  WITH NOCHECK ADD  CONSTRAINT FK_T_Products_i18n_T_Products FOREIGN KEY(PROD_i18n_PROD_Id)
ALTER TABLE dbo.T_Products_i18n  
    ADD CONSTRAINT FK_T_Products_i18n_T_Products 
    FOREIGN KEY(PROD_i18n_PROD_Id)
    REFERENCES dbo.T_Products (PROD_Id)
ON DELETE CASCADE 
GO

ALTER TABLE dbo.T_Products_i18n CHECK CONSTRAINT FK_T_Products_i18n_T_Products
GO

ALTER TABLE dbo.T_Products_i18n 
    ADD  CONSTRAINT FK_T_Products_i18n_T_Languages 
    FOREIGN KEY( PROD_i18n_Lang_Id )
    REFERENCES dbo.T_Languages( Lang_ID )
ON DELETE CASCADE 
GO

ALTER TABLE dbo.T_Products_i18n CHECK CONSTRAINT FK_T_Products_i18n_T_Products
GO



CREATE TABLE dbo.T_Products_i18n_Cust
(
     PROD_i18n_Cust_PROD_Id int NOT NULL
    ,PROD_i18n_Cust_Lang_Id int NOT NULL
    ,PROD_i18n_Cust_Text national character varying(200) NULL
    ,CONSTRAINT PK_T_Products_i18n_Cust PRIMARY KEY ( PROD_i18n_Cust_PROD_Id, PROD_i18n_Cust_Lang_Id )
);

GO

ALTER TABLE dbo.T_Products_i18n_Cust  
    ADD CONSTRAINT FK_T_Products_i18n_Cust_T_Languages 
    FOREIGN KEY(PROD_i18n_Cust_Lang_Id)
    REFERENCES dbo.T_Languages (Lang_ID)

ALTER TABLE dbo.T_Products_i18n_Cust CHECK CONSTRAINT FK_T_Products_i18n_Cust_T_Languages

GO



ALTER TABLE dbo.T_Products_i18n_Cust  
    ADD CONSTRAINT FK_T_Products_i18n_Cust_T_Products 
    FOREIGN KEY(PROD_i18n_Cust_PROD_Id)
REFERENCES dbo.T_Products (PROD_Id)
GO

ALTER TABLE dbo.T_Products_i18n_Cust CHECK CONSTRAINT FK_T_Products_i18n_Cust_T_Products
GO

然后填写数据

DELETE FROM T_Languages;
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (1, N'English', N'English', N'EN');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (2, N'Deutsch', N'German', N'DE');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (3, N'Français', N'French', N'FR');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (4, N'Italiano', N'Italian', N'IT');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (5, N'Russki', N'Russian', N'RU');
INSERT INTO T_Languages (Lang_ID, Lang_NativeName, Lang_EnglishName, Lang_ISO_TwoLetterName) VALUES (6, N'Zhungwen', N'Chinese', N'ZH');

DELETE FROM T_Products;
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (1, N'Orange Juice');
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (2, N'Apple Juice');
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (3, N'Banana Juice');
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (4, N'Tomato Juice');
INSERT INTO T_Products (PROD_Id, PROD_InternalName) VALUES (5, N'Generic Fruit Juice');

DELETE FROM T_Products_i18n;
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (1, 1, N'Orange Juice');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (1, 2, N'Orangensaft');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (1, 3, N'Jus d''Orange');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (1, 4, N'Succo d''arancia');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (2, 1, N'Apple Juice');
INSERT INTO T_Products_i18n (PROD_i18n_PROD_Id, PROD_i18n_Lang_Id, PROD_i18n_Text) VALUES (2, 2, N'Apfelsaft');

DELETE FROM T_Products_i18n_Cust;
INSERT INTO T_Products_i18n_Cust (PROD_i18n_Cust_PROD_Id, PROD_i18n_Cust_Lang_Id, PROD_i18n_Cust_Text) VALUES (1, 2, N'Orangäsaft'); -- Swiss German, if you wonder

然后查询数据:

DECLARE @__in_lang_id int
SET @__in_lang_id = (
    SELECT Lang_ID
    FROM T_Languages
    WHERE Lang_ISO_TwoLetterName = 'DE'
)

SELECT 
     PROD_Id 
    ,PROD_InternalName -- Default Fallback field (internal name/one language only setup), just in ResultSet for demo-purposes
    ,PROD_i18n_Text  -- Translation text, just in ResultSet for demo-purposes
    ,PROD_i18n_Cust_Text  -- Custom Translations (e.g. per customer) Just in ResultSet for demo-purposes
    ,COALESCE(PROD_i18n_Cust_Text, PROD_i18n_Text, PROD_InternalName) AS DisplayText -- What we actually want to show 
FROM T_Products 

LEFT JOIN T_Products_i18n 
    ON PROD_i18n_PROD_Id = T_Products.PROD_Id 
    AND PROD_i18n_Lang_Id = @__in_lang_id 

LEFT JOIN T_Products_i18n_Cust 
    ON PROD_i18n_Cust_PROD_Id = T_Products.PROD_Id
    AND PROD_i18n_Cust_Lang_Id = @__in_lang_id

如果您很懒,那么您也可以使用ISO-TwoLetterName('DE','EN'等)作为语言表的主键,则不必查找语言ID。但是,如果这样做,则可能要改用IETF语言标签,这样会更好,因为您会获得de-CH和de-DE,这在地形学上确实不一样(双s而不是ß) ,尽管它是相同的基本语言。这只是一个对您可能很重要的小细节,尤其是考虑到en-US和en-GB / en-CA / en-AU或fr-FR / fr-CA也有类似的问题。
Quote:我们不需要它,我们只用英语做我们的软件。
答:是的,但是哪一个?

无论如何,如果您使用整数ID,则表示您很灵活,可以在以后的任何时间更改方法。
您应该使用该整数,因为没有什么比拙劣的Db设计更令人讨厌,更具破坏性和麻烦的了。

另请参阅RFC 5646ISO 639-2

而且,如果您仍在说“我们” 针对“仅一种文化”(通常是en-US)提出申请-因此,我不需要多余的整数,那么这是提及该主题的好时机和地点IANA语言标签,不是吗?
因为他们像这样去:

de-DE-1901
de-DE-1996

de-CH-1901
de-CH-1996

(1996年进行了拼字法改革...)如果拼写错误,请尝试在字典中查找该单词;这在处理法律和公共服务门户的应用程序中变得非常重要。
更重要的是,有些地区从西里尔字母到拉丁字母正在变化,这可能比一些晦涩的拼字法改革的表面麻烦更为麻烦,这就是为什么这也可能是重要的考虑因素,这取决于您所居住的国家/地区。一种或另一种,最好在其中有该整数,以防万一...

编辑:
ON DELETE CASCADE 在之后添加

REFERENCES dbo.T_Products( PROD_Id )

您可以简单地说:DELETE FROM T_Products,而不会违反外键。

至于整理,我会这样做:

A)拥有自己的DAL
B)在语言表中保存所需的排序规则名称

您可能希望将排序规则放在自己的表中,例如:

SELECT * FROM sys.fn_helpcollations() 
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%' 

C)在您的auth.user.language信息中提供排序规则名称

D)这样写你的SQL:

SELECT 
    COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
FROM T_Groups 

ORDER BY GroupName COLLATE {#COLLATION}

E)然后,您可以在DAL中执行此操作:

cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)

然后,这将为您提供完美组成的SQL查询

SELECT 
    COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
FROM T_Groups 

ORDER BY GroupName COLLATE German_PhoneBook_CI_AI

详细的回复很好,非常感谢。但是您如何看待方法5解决方案中的排序规则问题。在需要使用不同排序规则的多语言环境中排序或过滤翻译后的文本时,这似乎不是最佳方法。在这种情况下,方法2(您很快“被排斥” :))可能是一个更好的选择,只需稍加修改即可指示每个局部列的目标排序规则。
Eugene Evdokimov,2015年

2
@Eugene Evdokimov:是的,但是“ ORDER BY”总是会出现问题,因为您不能将其指定为变量。我的方法是将排序规则名称保存在语言表中,并将其保存在userinfo中。然后,在每个SQL语句上,您都可以说ORDER BY COLUMN_NAME {#collat​​ion},然后可以在dal(cmd.CommandText = cmd.CommandText.Replace(“ {#COLLATION}”,auth.user)中进行替换。 –或者,您可以在应用程序代码中进行排序,例如使用LINQ,这也将减轻数据库的处理负担。对于报表,无论如何报表都将进行排序
Stefan Steiger

oo这一定是我所见过的最长的SO答案,而且我看到人们在答案中编写了整个程序。你很厉害。
Domino

能完全同意SunWuKung的解决方案是最好的
多米

48

第三种选择是最好的,原因如下:

  • 不需要更改数据库模式以使用新语言(从而限制了代码更改)
  • 不需要大量空间用于未实现的语言或特定项目的翻译
  • 提供最大的灵活性
  • 您不会得到稀疏表
  • 您不必担心null键,也不必检查您是否正在显示现有翻译而不是某些null输入。
  • 如果您更改或扩展数据库以包含其他可翻译的项目/事物/等,则可以使用相同的表和系统-这与其余数据完全无关。

-亚当


1
我同意,尽管就我个人而言,每个主表都有一个本地化的表,以允许实现外键。
尼尔·巴恩韦尔

1
尽管第三个选择是最干净,最合理的解决方案,但比第一个更复杂。我认为显示,编辑和报告通用版本需要付出额外的努力,以致于它并不总是可以接受的。我已经实现了这两种解决方案,当用户需要对“主要”应用程序语言进行只读(有时会丢失)翻译时,简单得多就足够了。
rics

12
如果product表包含多个翻译字段怎么办?检索产品时,每个翻译字段都必须执行一个附加连接,这将导致严重的性能问题。插入/更新/删除也有(IMO)额外的复杂性。这样做的唯一好处是减少了表的数量。我会选择SunWuKung提出的方法:我认为这是性能,复杂性和维护问题之间的良好平衡。
Frosty Z

@ rics-我同意,您对...有何建议?
sabre

@ Adam-我很困惑,也许我误会了。你建议了第三个,对吗?请更详细地说明这些表之间的关系如何?您是说我们必须为DB中的每个表实现Translation和TranslationEntry表吗?
sabre

9

看一下这个例子:

PRODUCTS (
    id   
    price
    created_at
)

LANGUAGES (
    id   
    title
)

TRANSLATIONS (
    id           (// id of translation, UNIQUE)
    language_id  (// id of desired language)
    table_name   (// any table, in this case PRODUCTS)
    item_id      (// id of item in PRODUCTS)
    field_name   (// fields to be translated)
    translation  (// translation text goes here)
)

我认为无需解释,该结构可以自我描述。


很好 但是您将如何搜索(例如product_name)?
Illuminati

您的样本中是否有现场示例?使用它是否遇到任何问题?
DavidLétourneau,2016年

当然,我有多语种房地产项目,我们支持4种语言。搜索有点复杂,但速度很快。当然,在大型项目中,它可能比需要的要慢。在中小型项目中也可以。
bamburik '16

8

我通常会采用这种方法(不是实际的sql),这与您的最后一个选项相对应。

table Product
productid INT PK, price DECIMAL, translationid INT FK

table Translation
translationid INT PK

table TranslationItem
translationitemid INT PK, translationid INT FK, text VARCHAR, languagecode CHAR(2)

view ProductView
select * from Product
inner join Translation
inner join TranslationItem
where languagecode='en'

因为将所有可翻译的文本放在一个位置使维护变得非常容易。有时翻译会外包给翻译局,这样您就可以只将它们发送一个大的导出文件,然后将其导入回去就很容易。


1
什么用途的Translation表或TranslationItem.translationitemid列服务?
DanMan

4

在转到技术细节和解决方案之前,您应该停一会儿,并询问一些有关要求的问题。答案可能会对技术解决方案产生巨大影响。这样的问题的例子有:
-是否会一直使用所有语言?
-谁以及何时将用不同的语言版本填充列?
-当用户需要某种语言的文本而系统中没有语言时,会发生什么?
-仅文本要本地化,或者还有其他项目(例如PRICE可以存储在$和€中,因为它们可能有所不同)


我知道本地化是一个更广泛的主题,并且我知道您引起了我的注意,但是目前我正在寻找一个非常具体的模式设计问题的答案。我假设将逐步增加新的语言,并且每种语言几乎都会被翻译。
qbeuek,

3

我在寻找一些本地化的技巧,并找到了这个主题。我想知道为什么要使用它:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

因此您得到类似user39603的提示:

table Product
productid INT PK, price DECIMAL, translationid INT FK

table Translation
translationid INT PK

table TranslationItem
translationitemid INT PK, translationid INT FK, text VARCHAR, languagecode CHAR(2)

view ProductView
select * from Product
inner join Translation
inner join TranslationItem
where languagecode='en'

您不能只留下表格Translation,所以得到以下内容:

    table Product
    productid INT PK, price DECIMAL

    table ProductItem
    productitemid INT PK, productid INT FK, text VARCHAR, languagecode CHAR(2)

    view ProductView
    select * from Product
    inner join ProductItem
    where languagecode='en'

1
当然。我会把ProductItem桌子叫做类似ProductTexts或类似的东西ProductL10n。更有道理。
DanMan

1

我同意随机分配器。我不明白为什么需要表格“ translation”。

我认为,这就足够了:

TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName

1

以下方法可行吗?假设您有需要翻译多于1列的表。因此,对于产品,您可以同时具有需要翻译的产品名称和产品说明。您可以执行以下操作:

CREATE TABLE translation_entry (
      translation_id        int,
      language_id           int,
      table_name            nvarchar(200),
      table_column_name     nvarchar(200),
      table_row_id          bigint,
      translated_text       ntext
    )

    CREATE TABLE translation_language (
      id int,
      language_code CHAR(2)
    )   

0

“哪个最好”是基于项目情况的。第一个易于选择和维护,并且性能最佳,因为在选择实体时不需要连接表。如果您确认自己的项目仅支持2种或3种语言,并且不会增加,则可以使用它。

第二个是好的,但很难理解和维护。而且性能比第一个差。

最后一个擅长于可伸缩性,但不擅长于性能。T_TRANSLATION_ENTRY表将变得越来越大,当您想从某些表中检索实体列表时,这将非常糟糕。


0

文档介绍了可能的解决方案以及每种方法的优缺点。我更喜欢“行本地化”,因为添加新语言时不必修改数据库模式。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.