本地化SQL Server(2005/2008)数据库的最佳实践

时间:2008-11-03 12:26:39

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

问题

我相信很多人都面临着将数据库后端本地化到应用程序的挑战。如果你没有那么我会非常自信地说你将来必须这样做的可能性非常大。我正在谈论为您的数据库实体存储多个文本翻译(对货币等也是如此)。

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

这为您留下了许多“文本”表,每个表对应您想要本地化的每个实体,并添加了一个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思想的评论,都会非常有用。

10 个答案:

答案 0 :(得分:3)

我认为您可以坚持使用XML,从而实现更清晰的设计。我会更进一步,利用is designed for this usage

xml: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(如评论中所示)在查询中选择本地化资源,以避免任何客户端处理。这会产生类似的结果(未经测试):

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

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

答案 1 :(得分:2)

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

我有一个产品表:

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

和products_globalization表:

* id
* products_id
* name
* shortdescription
* longdescription

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

我更喜欢在全局资源表上有一个并排表,因为在某些情况下你可能需要做几个列上的数据库(MySQL)MATCH,例如MATCH(名称,短描述,长描述)反对('这里的东西')。

在正常情况下,某些产品翻译可能会丢失,但我仍想显示所有产品(不仅仅是翻译过的产品)。因此,对连接进行操作是不够的,我们实际上需要根据products-table进行左连接。

伪:

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,但除此之外。

嗯,这就是我的看法。随意批评我的建议......

亲切的问候, 理查德

答案 2 :(得分:1)

我看不出为什么你需要多个文本表。具有“全局”唯一文本ID的单个文本表应该足够了。该表将具有ID,语言,文本列,您只能使用您需要呈现的语言(或者根本不能检索文本)获取文本。连接应该相当有效,因为(ID,语言)的组合是主键。

答案 3 :(得分:1)

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

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

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

  1. 将本地化项目存储在可执行文件(本地化资源dll)附近
  2. 将本地化项目存储在数据库中,并在包含本地化项目的表中引入localeID列。
  3. 第一种方法的优点是VisualStudio的良好支持。第二个优点是集中部署。

答案 4 :(得分:1)

我认为使用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解决方案,您将获得包含所请求文化的所有值的整个记录​​。

答案 5 :(得分:0)

我喜欢XML方法,因为单独的表解决方案不会返回结果,例如除非你做外连接,否则没有瑞典语翻译(cultureID = 1)。但是你不能回到英语。使用XML方法,您可以简单地使用英语。 在生产环境中有关XML方法的任何新闻吗?

答案 6 :(得分:0)

这里有一些关于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]

GO

 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                             

答案 7 :(得分:0)

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

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

主要缺点是从数据库传输到服务层/ UI / App的数据量。我会尝试在返回结果之前对SQL端进行一些转换,只返回一个文化UI。您可以随时通过sproc中的xml选择当前文化,并将其作为普通文本返回。

总体而言,这与博客文章或CMS对本地化的需求有所不同 - 我已经做了一些。

我对Post scene的方法与TToni相似,只是从Domain的角度(以及一点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在博客上并不重要,因为每个帖子都绑定了不同的语言。如果有副本被转录,那么这里也可以正常工作,因为每个帖子都是该文化所独有的,因此会得到一组独特的评论等。

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

答案 8 :(得分:0)

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

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

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

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

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

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

另外,如果您正在运行电子商务商店并且实际上希望将本地化页面编入索引,则必须略微偏离Microsoft创建的看似自然的方式。实际和逻辑设计流程与SEO Google recommends之间存在明显的分歧。事实上,一些网站管理员抱怨他们的网页没有被搜索引擎索引,除了“默认”文化之外的任何东西,因为搜索引擎只会将一个网址索引一次,即使它根据浏览器的文化而改变。

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

答案 9 :(得分:0)

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