【问题标题】:Best-practices for localizing a SQL Server (2005/2008) database本地化 SQL Server (2005/2008) 数据库的最佳实践
【发布时间】:2008-11-03 12:26:39
【问题描述】:

问题

我相信你们中的许多人都面临过将数据库后端本地化到应用程序的挑战。如果你还没有,那么我很有信心说你将来必须这样做的可能性很大。我说的是为您的数据库实体存储多个文本翻译(货币等也是如此)。

例如,经典的“类别”表可能有一个名称和一个说明列,它们应该是全球化的。一种方法是为每个实体创建一个“文本”表,然后根据提供的语言进行连接以检索值。

这为您留下了很多“文本”表,一个用于您要本地化的每个实体,并添加了一个 TextType 来区分它可能存储的各种文本。

我很好奇在 SQL Server 2005/2008 数据库中实现这种支持是否有任何记录在案的最佳实践/设计模式(我特别关注 RDBMS,因为它可能包含受支持的关键字和这样有助于实施)?

对 XML 方法的思考

我一直在玩弄的一个想法(尽管到目前为止只是在我的脑海中)是利用 SQL Server 2005 中引入的 XML 数据类型。这个想法是制作应该支持本地化的 XML 数据类型的列(并绑定一个架构)。 XML 将包含本地化字符串以及与之相关的语言代码/文化。

类似的东西

Product
ID (int, identity)
Name (XML ...)
Description (XML ...)

那么你会得到像这样的 XML

<localization>
  <text culture="sv-SE">Detta är ett namn</text>
  <text culture="en-EN">This is a name</text>
</localization>

然后你可以这样做(这不是生产代码,所以我将使用 *)

SELECT *
From Product
Where Product.ID = 10

您将获得带有所有本地化文本的产品,这意味着您必须在客户端进行提取。这里最大的问题显然是您必须在每个查询中返回的额外数据量,其好处是没有查找表、连接等的更简洁的设计。

顺便说一句,无论我最终在设计中使用什么方法,我仍将使用 Linq To SQL(.NET 平台)来查询数据库(XML 方法应该是一个问题,因为它会返回一个 XElement,它可能是解释客户端)

所以关于数据库本地化设计模式的建议,以及可能的关于 XML 思想的 cmets,将非常受欢迎。

【问题讨论】:

  • 我没有这方面的经验(以及为什么这不是一个答案),但这看起来是一个非常简单和好的解决方案。您可以通过使用 XPath 查询来仅获取所需的语言来减少开销。但是,不确定搜索/索引如何与此一起使用。
  • 您可以在服务器上使用 XQUery 来搜索 XML 列,还可以在列上添加模式以确保类型安全。感谢您的编辑!
  • 我真的很喜欢这个主意。你有什么问题吗?

标签: sql-server linq-to-sql database-design localization


【解决方案1】:

我认为您可以坚持使用允许更简洁设计的 XML。我会更进一步并利用is designed for this usagexml:lang 属性:

<l10n>
  <text xml:lang="sv-SE">Detta är ett namn</text>
  <text xml:lang="en-EN">This is a name</text>
</l10n>

更进一步,您可以通过a XPath query(如 cmets 中的建议)在查询中选择本地化资源,以避免任何客户端处理。这会给出这样的东西(未经测试):

SELECT Name.value('(l10n/text[lang()="en"])[1]', 'NVARCHAR(MAX)')
  FROM Product
  WHERE Product.ID=10;

请注意,与单独的表格相比,此解决方案将是一种优雅但效率较低的解决方案。这对于某些应用程序来说可能没问题。

【讨论】:

  • 我没有在 SQL 中使用 XPath 查询,所以我很好奇采用这种设计的更新会如何。
【解决方案2】:

这是我的做法。 我不使用 LINQ 或 SP,因为查询太复杂并且是动态构建的,这只是查询的摘录。

我有一个产品表:

* id
* price
* stocklevel
* active
* name
* shortdescription
* longdescription

还有一个 products_globalization 表:

* id
* products_id
* name
* shortdescription
* longdescription

如您所见,产品表也包含所有全球化列。这些列包含默认语言(因此,在请求默认文化时能够跳过连接 - 但我不确定这是否值得麻烦,我的意思是两个表之间的连接是基于索引的所以。 .. - 给我一些反馈)。

我更喜欢在全局资源表上使用并排表,因为在某些情况下,您可能需要在几列上执行数据库 (MySQL) MATCH,例如 MATCH(name, shortdescription, longdescription)反对(“这里有东西”)。

在正常情况下,某些产品翻译可能会丢失,但我仍想显示所有产品(不仅仅是已翻译的产品)。所以只做一个join是不够的,我们实际上需要根据products-table做一个left join。

伪:

string query = "";
if(string.IsNullOrEmpty(culture)) {
   // No culture specified, no join needed.
   query = "SELECT p.price, p.name, p.shortdescription FROM products p WHERE p.price > ?Price";
} else {
   query = "SELECT p.price, case when pg.name is null then p.name else pg.name end as 'name', case when pg.shortdescription is null then p.shortdescription else pg.shortdescription end as 'shortdescription' FROM products p"
   + " LEFT JOIN products_globalization pg ON pg.products_id = p.id AND pg.culture = ?Culture"
   + " WHERE p.price > ?Price";
}

我会选择 COALESCE 而不是 CASE ELSE,但这不是重点。

嗯,这就是我的看法。欢迎批评我的建议...

亲切的问候, 理查德

【讨论】:

    【解决方案3】:

    我不明白您为什么需要多个文本表。具有“全局”唯一文本 ID 的单个文本表就足够了。该表将具有 ID、语言、文本列,并且您只会获得您需要呈现的语言的文本(或者可能根本不检索文本)。连接应该相当有效,因为 (ID, language) 的组合是主键。

    【讨论】:

    • 我对这种设计有几点反对意见: 1. 不仅文本可能具有本地化值。 2. 考虑帖子中的示例 Select。而不是获得名称和描述,您将获得必须进行第二次往返的 Id。或者您将不得不使用子选择。 3.调试很痛苦
    • 1.例如? 2. SELECT Product.ID, Product.Name, Text.Message from Product, Text where Product.Description = Text.ID (and Text.Lang="En"); 3. 怎么会?
    • 您的建议是灵活的,但是,如果您使用的是 ORM,它可能就没那么有用了...
    【解决方案4】:

    这是很难回答的问题之一,因为答案中有很多“取决于”:-)

    答案取决于数据库中本地化项目的数量、部署方案、缓存问题、访问模式等。如果您能给我们一些关于应用程序有多大、将有多少并发用户以及如何部署的数据,那将非常有帮助。

    一般来说,我通常使用以下两种方法之一:

    1. 将本地化项目存储在可执行文件附近(本地化资源 dll)
    2. 将本地化项目存储在数据库中,并在包含本地化项目的表中引入 localeID 列。

    第一种方法的优点是对 VisualStudio 的良好支持。第二种的优点是集中部署。

    【讨论】:

    • 好吧,请考虑一下您拥有类别、产品等的平均电子商务。每个实体都可能有名称、描述等(标准香草材料)列。值必须在数据库中,问题是设计以及是否有任何最佳实践/模式
    • 好的,这并没有真正的帮助,但无论如何我会尝试第二个答案:-)
    【解决方案5】:

    我认为使用 XML 列存储本地化值没有任何优势。除非你有一个项目的所有本地化版本“在一个地方”,如果这对你有价值的话。

    我建议在每个具有可本地化项目的表中使用cultureID 列。这样,您根本不需要任何 XML 处理。您已经将数据保存在关系模式中,那么当关系模式完全能够处理问题时,为什么还要引入另一层复杂性呢?

    假设“sv-SE”的cultureID = 1,“en-EN”的值为2。

    那么您的查询将被修改为

    SELECT *
    From Product
    Where Product.ID = 10 AND Product.cultureID = 1
    

    对于瑞典客户。

    我在本地化数据库中经常看到这种解决方案。它可以很好地适应文化数量和数据记录数量。它避免了 XML 的解析和处理,并且易于实现。

    还有一点:XML 解决方案为您提供了您不需要的灵活性:例如,您可以从“名称”列和“en-EN”值中获取“sv-SE”值来自“描述”列。但是,您不需要这个,因为您的客户一次只会请求一种文化。灵活性通常是有代价的。在这种情况下,您需要单独解析所有列,同时使用cultureID 解决方案,您可以获得包含所有值的整个记录​​,其中所有值都适合所请求的文化。

    【讨论】:

    • 这个建议有问题。为什么像 Product 这样的实体需要引用文化/语言环境?我们是否假设产品的所有属性都是可本地化的??
    • @Yarik:我相信他推荐数据库中的两条记录用于单个产品 ID“10”。每条记录将具有不同的cultureID。通过在数据库中拥有每个本地化版本的实例,这将消除对原始作者提到的“文本”表的需求。 TToni 方法的缺点是您不再拥有用于聚集索引的唯一 ProductID。您必须在 ProductID 和 CultureID 上双键才能恢复性能。而且,您的所有查询都必须考虑到文化。
    【解决方案6】:

    我喜欢 XML 方法,因为分离表解决方案不会返回结果,例如除非您进行外部联接,否则没有瑞典语翻译 (cultureID = 1)。但尽管如此,你不能回退到英语。使用 XML 方法,您可以简单地回退到英语。 有关于在生产环境中使用 XML 方法的消息吗?

    【讨论】:

      【解决方案7】:

      这里有一些关于 Rick Strahl 博客的想法:

      Localization of database Localization of JavaScript

      我更喜欢在 UserSetting 表中使用单个开关,它通过调用存储过程来使用......这里有一些代码

      CREATE TABLE [dbo].[Lang_en_US_Msg](
          [MsgId] [int] IDENTITY(1,1) NOT NULL,
          [MsgKey] [varchar](200) NOT NULL,
          [MsgTxt] [varchar](2000) NOT NULL,
          [MsgDescription] [varchar](2000) NOT NULL,
       CONSTRAINT [PK_Lang_US-us__Msg] PRIMARY KEY CLUSTERED 
      (
          [MsgId] 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].[User](
          [UserId] [int] IDENTITY(1,1) NOT NULL,
          [FirstName] [varchar](50) NOT NULL,
          [MiddleName] [varchar](50) NULL,
          [LastName] [varchar](50) NULL,
          [DomainName] [varchar](50) NULL,
       CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
      (
          [UserId] ASC
      )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
      ) ON [PRIMARY]
      
      CREATE TABLE [dbo].[UserSetting](
          [UserSettingId] [int] IDENTITY(1,1) NOT NULL,
          [UserId] [int] NOT NULL,
          [CultureInfo] [varchar](50) NOT NULL,
          [GuiLanguage] [varchar](10) NOT NULL,
       CONSTRAINT [PK_UserSetting] PRIMARY KEY CLUSTERED 
      (
          [UserSettingId] ASC
      )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
      ) ON [PRIMARY]
      

       ALTER TABLE [dbo].[UserSetting] ADD  CONSTRAINT [DF_UserSetting_CultureInfo]  DEFAULT ('fi-FI') FOR [CultureInfo]
       GO
      
       CREATE TABLE [dbo].[Lang_fi_FI_Msg](
          [MsgId] [int] IDENTITY(1,1) NOT NULL,
          [MsgKey] [varchar](200) NOT NULL,
          [MsgTxt] [varchar](2000) NOT NULL,
          [MsgDescription] [varchar](2000) NOT NULL,
          [DbSysNameForExpansion] [varchar](50) NULL,
       CONSTRAINT [PK_Lang_Fi-fi__Msg] PRIMARY KEY CLUSTERED 
      (
          [MsgId] ASC
      )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
      ) ON [PRIMARY]
      
      CREATE PROCEDURE [dbo].[procGui_GetPageMsgs]
      @domainUser varchar(50) ,           -- the domain_user performing the action  
      @msgOut varchar(4000) OUT,        -- the (error) msg to be shown to the user   
      @debugMsgOut varchar(4000) OUT ,   -- this variable holds the debug msg to be shown if debug level is enabled   
      @ret int OUT                  -- the variable indicating success or failure 
      
      AS                            
      BEGIN -- proc start                            
       SET NOCOUNT ON;                            
      
      declare @procedureName varchar(200)        
      declare @procStep varchar(4000)  
      
      
      set @procedureName = ( SELECT OBJECT_NAME(@@PROCID))        
      set @msgOut = ' '     
      set @debugMsgOut = ' '     
      set @procStep = ' '     
      
      
      BEGIN TRY        --begin try                  
      set @ret = 1 --assume false from the beginning                  
      
      --===============================================================
       --debug   set @procStep=@procStep + 'GETTING THE GUI LANGUAGE FOR THIS USER '
      --===============================================================
      
      declare @guiLanguage nvarchar(10)
      
      
      
      
      if ( @domainUser is null)
          set @guiLanguage = (select Val from AppSetting where Name='guiLanguage')
      else 
          set @guiLanguage = (select GuiLanguage from UserSetting us join [User] u on u.UserId = us.UserId where u.DomainName=@domainUser)
      
      set @guiLanguage = REPLACE ( @guiLanguage , '-' , '_' ) ;
      
      
      --===============================================================
      set @procStep=@procStep + ' BUILDING THE SQL QUERY '
      --===============================================================
      
      DECLARE @sqlQuery AS nvarchar(2000)
      SET @sqlQuery = 'SELECT  MsgKey , MsgTxt FROM dbo.lang_' + @guiLanguage + '_Msg'
      
      
      --===============================================================
      set @procStep=@procStep + 'EXECUTING THE SQL QUERY'
      --===============================================================
      print @sqlQuery
      
          exec sp_executesql @sqlQuery
      
          set @debugMsgOut = @procStep
          set @ret = @@ERROR                  
      
      
      END TRY        --end try                  
      
      BEGIN CATCH                        
       PRINT 'In CATCH block.                         
       Error number: ' + CAST(ERROR_NUMBER() AS varchar(10)) + '                        
       Error message: ' + ERROR_MESSAGE() + '                        
       Error severity: ' + CAST(ERROR_SEVERITY() AS varchar(10)) + '                        
       Error state: ' + CAST(ERROR_STATE() AS varchar(10)) + '                        
       XACT_STATE: ' + CAST(XACT_STATE() AS varchar(10));                        
      
      set @msgOut = 'Failed to execute ' + @sqlQuery             
      set @debugMsgOut = ' Error number: ' + CAST(ERROR_NUMBER() AS varchar(10)) +               
       'Error message: ' + ERROR_MESSAGE() + 'Error severity: ' + CAST(ERROR_SEVERITY() AS varchar(10)) +               
       'Error state: ' + CAST(ERROR_STATE() AS varchar(10)) + 'XACT_STATE: ' + CAST(XACT_STATE() AS varchar(10))                        
      
      --record the error in the database                        
      --debug    
       --EXEC [dbo].[procUtils_DebugDb]
          --  @DomainUser = @domainUser,
          --  @debugmsg = @debugMsgOut,
          --  @ret = 1,
          --  @procedureName = @procedureName ,
          --  @procedureStep = @procStep
      
       -- set @ret = 1                       
      
      END CATCH                        
      
      
      return  @ret                                   
      END --procedure end                             
      

      【讨论】:

        【解决方案8】:

        我看到了总体上的差异 - 您必须将单个实体表示为单个实例(例如,一个 ProductID 为“10”),但具有不同列/属性的多个本地化文本。这是一个艰难的过程,我确实看到了对 POS 系统的需求,即您只想跟踪一个 ProductID = 10,而不是具有不同 ProductID 的多个产品,而是具有不同文本的同一事物。

        我倾向于您和其他人已经在此处概述的 XML 列解决方案。是的,它通过网络传输更多数据 - 但是,它使事情变得简单,并且如果数据包站点成为问题,可以使用 XElement 进行过滤。

        主要缺点是通过线路从数据库传输到服务层/UI/应用程序的数据量。在返回结果之前,我会尝试在 SQL 端进行一些转换,只返回一种文化 UI。您总是可以通过 xml 在 sproc 中选择正确的文化,并将其作为普通文本返回。

        总体而言,这与博客文章或 CMS 需要本地化不同 - 我已经做过一些。

        我对 Post 场景的处理方法与 TToni 的方法类似,只是从域的角度对数据进行建模(以及一点 BDD)。话虽如此,请专注于您想要实现的目标:

        Given a users culture is "sv-se"
        When the user views a post list
        It should list posts only in "sv-se" culture
        

        这意味着用户应该只看到与其文化相关的帖子列表。我们之前实现的方式是传入一组文化,以根据用户可以看到的内容进行查询。如果用户将“sv-se”设置为他们的主要用户,但也选择了他们说美国英语 (en-us),那么查询将是:

        SELECT * FROM Post WHERE CultureUI IN ('sv-se', 'en-us')
        

        请注意,这如何为您提供所有帖子及其不同的 PostID,这是该语言独有的。 PostID 在博客上并不那么重要,因为每个帖子都绑定到不同的语言。如果有副本被转录,那么在这里也可以正常工作,因为每个帖子都是该文化所独有的,因此会获得一组独特的 cmets 等。

        但是回到我的答案的第一部分,您的需求源于需要具有多个文本的单个实例的要求。 Xml 列非常适合。

        【讨论】:

          【解决方案9】:

          要考虑的另一种方法:不要将内容存储在数据库中,而是将支持数据库记录的“应用程序”和“内容”作为单独的实体。

          在为我的电子商务网站创建多个主题时,我使用了与此类似的方法。一些产品具有制造商徽标,该徽标也必须与网站主题相匹配。由于主题没有真正的数据库支持,所以我遇到了问题。我想出的解决方案是使用数据库中的令牌来识别图像的 ClientID,而不是存储图像的 URL(这会因主题而异)。

          按照相同的方法,您可以将数据库从存储产品的名称和描述更改为存储名称标记和可识别资源的描述标记(在 resx 文件中或使用 Rick Strahl 方法的数据库中)包含内容。 .NET 的内置功能然后会处理语言切换,而不是尝试在数据库中进行(将业务逻辑放在数据库中很少是一个好主意)。然后,您可以使用客户端上的令牌来查找正确的资源。

          Label1.Text = GetLocalResourceObject("TokenStoredInDatabase").ToString()
          

          这种方法的缺点显然是使数据库令牌和资源令牌保持同步(因为可以在没有任何描述的情况下添加产品),但使用诸如 Rick Strahl 创建的资源提供者可能会更容易完成。如果您的产品经常更换,这种方法可能行不通,但对某些人来说可能。

          优点是您有少量数据要从数据库传输到客户端,您的内容与数据库完全分离,并且您的数据库不需要比现在更复杂。

          附带说明,如果您正在经营一家电子商务商店,并且确实希望将您的本地化页面编入索引,那么您必须稍微偏离 Microsoft 创建的看似自然的方式。实用和合乎逻辑的设计流程与 SEO 的 Google recommends 之间显然存在分歧。事实上,一些网站管理员抱怨他们的页面没有被搜索引擎索引为“默认”文化,因为搜索引擎只会索引一次单个 URL,即使它根据浏览器的文化而改变。

          幸运的是,有一个简单的方法可以解决这个问题:在页面上放置链接,以根据查询字符串参数将其翻译成其他语言。可以找到一个这样的例子(哎呀,他们不会让我发布另一个链接!!)如果你检查一下,页面的每种文化都已被谷歌和雅虎索引(尽管不是必应)。更高级的方法可能是结合使用 URL 重写和一些花哨的正则表达式,使您的单个本地化页面看起来像是有多个目录,但实际上是向页面传递了一个查询字符串参数。

          【讨论】:

            【解决方案10】:

            索引成为一个问题。我不认为你可以索引xml,当然,如果你将它存储为字符串,你就不能索引它,因为每个字符串都会以&lt;localization&gt; &lt;text culture="..."&gt;开头。

            【讨论】:

              猜你喜欢
              • 2010-09-17
              • 1970-01-01
              • 1970-01-01
              • 2010-12-01
              • 1970-01-01
              • 1970-01-01
              • 2011-01-21
              • 2013-05-25
              • 2011-05-24
              相关资源
              最近更新 更多