多语言数据库的架构

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

但是,定义多语言数据库模式的最佳方法是什么? 假设我们有很多表(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)
    )
    
  • 每种解决方案都有优点和缺点,我想知道您对这些方法有什么经验,您有什么建议以及如何设计多语言数据库模式。


    您如何看待每个可翻译表的相关翻译表?

    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)

    这样,如果你有多个可翻译的列,它只需要一个单独的连接来获得它+,因为你不是自动生成翻译版本,它可能更容易导入项目及其相关翻译。

    不利的一面是,如果你有一个复杂的语言回退机制,你可能需要为每个转换表实现 - 如果你依靠某个存储过程来做到这一点。 如果你从应用程序这样做,这可能不会是一个问题。

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


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

  • 不需要更改新语言的数据库模式(从而限制代码更改)
  • 不需要大量的空间用于未实现的语言或特定项目的翻译
  • 提供最大的灵活性
  • 你不会得到稀疏的表格
  • 您不必担心空键,并检查您是否显示现有翻译,而不是一些空条目。
  • 如果您更改或扩展您的数据库以包含其他可翻译的项目/东西/等等,您可以使用相同的表格和系统 - 这与其余数据非常不兼容。
  • -亚当


    这是一个有趣的问题,所以让我们来看看死神。

    让我们从方法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:
    所有翻译的一张表:缺点:您必须在产品表中为要翻译的n个字段存储n个外键。 因此,您必须对n个字段进行n次连接。 当翻译表是全局的时候,它有很多条目,连接变慢。 此外,您总是必须将n个字段的T_TRANSLATION表加入n次。 这是相当的开销。 现在,当您必须为每位客户提供定制翻译时,您会做什么? 你必须在另外一个表上添加另外2个n连接。 如果您必须加入10个表格,并且需要2x2xn = 4n个额外的连接,那真是一团糟! 此外,这种设计可以使用2个表格的相同翻译。 如果我在一个表格中更改项目名称,是否真的要更改另一个表格中的条目以及每次单独使用?

    另外,您不能删除并重新插入表格,因为现在在产品表格中有外键...您当然可以省略设置FK,然后<insert name of your "favourite" person here>可以删除表格,然后用newid()重新插入所有条目[或者通过在插入中指定id,但是具有标识插入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 
    

    你可以用'.../' + @in_language + '/...'替换/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] [nvarchar](200) NULL,
        [Lang_EnglishName] [nvarchar](200) NULL,
        [Lang_ISO_TwoLetterName] [varchar](10) NULL,
     CONSTRAINT [PK_T_Languages] PRIMARY KEY CLUSTERED 
    (
        [Lang_ID] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    
    
    
    CREATE TABLE [dbo].[T_Products](
        [PROD_Id] [int] NOT NULL,
        [PROD_InternalName] [nvarchar](255) NULL,
     CONSTRAINT [PK_T_Products] PRIMARY KEY CLUSTERED 
    (
        [PROD_Id] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    
    
    
    
    CREATE TABLE [dbo].[T_Products_i18n](
        [PROD_i18n_PROD_Id] [int] NOT NULL,
        [PROD_i18n_Lang_Id] [int] NOT NULL,
        [PROD_i18n_Text] [nvarchar](200) NULL,
     CONSTRAINT [PK_T_Products_i18n] PRIMARY KEY CLUSTERED 
    (
        [PROD_i18n_PROD_Id] ASC,
        [PROD_i18n_Lang_Id] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    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]  WITH CHECK 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]  WITH CHECK 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_Languages]
    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] [nvarchar](200) NULL,
     CONSTRAINT [PK_T_Products_i18n_Cust] PRIMARY KEY CLUSTERED 
    (
        [PROD_i18n_Cust_PROD_Id] ASC,
        [PROD_i18n_Cust_Lang_Id] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    ALTER TABLE [dbo].[T_Products_i18n_Cust]  WITH CHECK ADD  CONSTRAINT [FK_T_Products_i18n_Cust_T_Languages] FOREIGN KEY([PROD_i18n_Cust_Lang_Id])
    REFERENCES [dbo].[T_Languages] ([Lang_ID])
    GO
    
    ALTER TABLE [dbo].[T_Products_i18n_Cust] CHECK CONSTRAINT [FK_T_Products_i18n_Cust_T_Languages]
    GO
    
    --ALTER TABLE [dbo].[T_Products_i18n_Cust]  WITH NOCHECK ADD  CONSTRAINT [FK_T_Products_i18n_Cust_T_Products] FOREIGN KEY([PROD_i18n_Cust_PROD_Id])
    ALTER TABLE [dbo].[T_Products_i18n_Cust]  WITH CHECK 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 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-Query

    SELECT 
        COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
    FROM T_Groups 
    
    ORDER BY GroupName COLLATE German_PhoneBook_CI_AI
    
    链接地址: http://www.djcxy.com/p/6387.html

    上一篇: Schema for a multilanguage database

    下一篇: Multilingual Database with Entity Framework 4 Guidance