当前位置:  开发笔记 > 编程语言 > 正文

多语言数据库的模式

如何解决《多语言数据库的模式》经验,为你挑选了5个好方法。

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

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

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

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

    每种语言的单独列

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

    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)
    )
    

    翻译表与每种语言的列

    不存储翻译的文本,而是仅存储翻译表的外键.转换表包含每种语言的列.

    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
    )
    

    用于每种语言的行的转换表

    不存储翻译的文本,而是仅存储翻译表的外键.转换表仅包含一个键,而一个单独的表包含每个语言转换的行.

    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)
    )
    

每个解决方案都有优缺点,我想知道您对这些方法的体验,您的建议是什么以及如何设计多语言数据库模式.



1> 小智..:

您如何为每个可翻译表格提供相关的翻译表?

CREATE TABLE T_PRODUCT(pr_id int,PRICE NUMBER(18,2))

CREATE TABLE T_PRODUCT_tr(pr_id INT FK,languagecode varchar,pr_name text,pr_descr text)

这样,如果您有多个可翻译列,则只需要一个连接即可获得+,因为您不自动生成翻译,因此可能更容易将项目与其相关翻译一起导入.

这方面的负面影响是,如果您有一个复杂的语言回退机制,您可能需要为每个转换表实现 - 如果您依赖某些存储过程来执行此操作.如果你从应用程序那样做,这可能不会是一个问题.

让我知道你的想法 - 我也将为我们的下一个申请做出决定.到目前为止,我们已经使用了你的第三种


它不需要新表用于新语言 - 只需使用新语言将新行添加到相应的_tr表中,如果创建新的可翻译表,则只需创建新的_tr表
我相信这是一个很好的方法.其他方法需要大量的左连接,当你连接多个表时,每个表都有3级深度的翻译,每个都有3个字段,你需要3*3 9个左连接只用于翻译..其他方面3.还有它更容易添加约束等,我相信搜索更合理.
此选项类似于我的选项nr 1但更好.它仍然很难维护,需要为新语言创建新表,所以我不愿意实现它.

2> Stefan Steig..:

这是一个有趣的问题,所以让我们来吧.

让我们从方法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)最重要的是,你放松了任何语法检查.如果改变了模式,因为创建突然机翼变化,迈上了一个新表的要求,旧人离开,但参考字段改名,你没有得到任何警告.在不选择wing参数(==> guid.empty)的情况下运行报表时,报表甚至可以正常工作.但突然之间,当一个真正的用户真正选择了一个翼==> 热潮.这种方法完全打破了任何类型的测试.


方法2:
简而言之:"好"的想法(警告 - 讽刺),让我们结合方法3的缺点(许多条目时速度慢)和方法1的相当可怕的缺点
.这种方法的唯一优点是你保持所有翻译都在一张桌子上,因此简化了维护工作.但是,使用方法1和动态SQL存储过程以及包含翻译的(可能是临时的)表以及目标表的名称可以实现同样的目的(假设您将所有文本字段命名为相同).


方法3:
所有翻译的一个表:缺点:您必须在产品表中为要翻译的n个字段存储n个外键.因此,您必须为n个字段进行n个连接.当转换表是全局的时,它有许多条目,并且连接变慢.此外,您必须为n个字段加入T_TRANSLATION表n次.这是一个相当大的开销.现在,当您必须为每个客户提供自定义翻译时,您会怎么做?您必须在另一个表上添加另外2个n个连接.如果你必须加入,比如10个表,2x2xn = 4n个额外的连接,真是一团糟!此外,这种设计使得可以使用2个表格的相同翻译.如果我在一个表中更改项目名称,我是否真的想在每个单独的时间更改另一个表中的条目?

另外,你不能再删除和重新插入表了,因为现在产品表中有外键...你当然可以省略设置FK,然后可以删除表,然后重新插入所有具有newid()的条目[或者通过在插入中指定id,但是将identity-insert设置为OFF ],这将很快(并且将)导致数据垃圾(和空引用异常).


方法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'Hello', 2) 
            --,CONVERT(XML, N'Hello', 2) 
            ,CONVERT(XML
            , N'

      Deutsch
      Français
      Ital&iano
      English

            ' 
            , 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 ""I am a ''value ""')
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,这真的不是同样的ortography(双s而不是ß到处) ,虽然它是相同的基本语言.这只是一个小小的细节,可能对你很重要,尤其是考虑到EN-US名称和en-GB/EN-CA/EN-AU或FR-FR/FR-CA也有类似的问题.
Quote:我们不需要它,我们只用英文做我们的软件.
答:是的 - 但是哪一个?

无论如何,如果你使用整数ID,你就很灵活,并且可以在以后改变你的方法.
你应该使用那个整数,因为没有什么比拙劣的Db设计更烦人,更具破坏性和麻烦.

另见RFC 5646,ISO 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


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

3> Adam Davis..:

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

不需要为新语言更改数据库模式(从而限制代码更改)

对于未实现的语言或特定项目的翻译,不需要大量空间

提供最大的灵活性

您最终不会使用稀疏表

您不必担心空键并检查您是否显示现有翻译而不是某些空条目.

如果您更改或扩展数据库以包含其他可翻译项目/事物/等,您可以使用相同的表和系统 - 这与其他数据非常不相关.

-亚当


如果产品表包含多个翻译字段怎么办?检索产品时,每个翻译字段必须再进行一次连接,这将导致严重的性能问题.插入/更新/删除还有(IMO)额外的复杂性.这样做的唯一优点是表的数量较少.我会选择SunWuKung提出的方法:我认为这是性能,复杂性和维护问题之间的良好平衡.

4> 小智..:

看一下这个例子:

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)
)

我认为没有必要解释,结构描述了自己.



5> user39603..:

我通常会采用这种方法(不是实际的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'

因为在一个地方拥有所有可翻译的文本使得维护变得更加容易.有时翻译会外包给翻译机构,这样您就可以向他们发送一个大的导出文件,并轻松地将其导回.

推荐阅读
周扒pi
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有