qbe*_*uek 222 multilingual database-design localization
我正在开发一种多语言软件.就应用程序代码而言,可本地化不是问题.我们可以使用特定于语言的资源,并拥有适合他们的各种工具.
但是,定义多语言数据库模式的最佳方法是什么?假设我们有很多表(100或更多),每个表可以有多个可以本地化的列(大多数nvarchar列应该是可本地化的).例如,其中一个表可能包含产品信息:
CREATE TABLE T_PRODUCT (
NAME NVARCHAR(50),
DESCRIPTION NTEXT,
PRICE NUMBER(18, 2)
)
Run Code Online (Sandbox Code Playgroud)
我可以想到三种方法来支持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)
)
Run Code Online (Sandbox Code Playgroud)翻译表与每种语言的列
不存储翻译的文本,而是仅存储翻译表的外键.转换表包含每种语言的列.
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
)
Run Code Online (Sandbox Code Playgroud)用于每种语言的行的转换表
不存储翻译的文本,而是仅存储翻译表的外键.转换表仅包含一个键,而一个单独的表包含每个语言转换的行.
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)
)
Run Code Online (Sandbox Code Playgroud)每个解决方案都有优缺点,我想知道您对这些方法的体验,您的建议是什么以及如何设计多语言数据库模式.
小智 110
您如何为每个可翻译表格提供相关的翻译表?
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)
这样,如果您有多个可翻译列,则只需要一个连接即可获得+,因为您不自动生成翻译,因此可能更容易将项目与其相关翻译一起导入.
这方面的负面影响是,如果您有一个复杂的语言回退机制,您可能需要为每个转换表实现 - 如果您依赖某些存储过程来执行此操作.如果你从应用程序那样做,这可能不会是一个问题.
让我知道你的想法 - 我也将为我们的下一个申请做出决定.到目前为止,我们已经使用了你的第三种
Ste*_*ger 50
这是一个有趣的问题,所以让我们来吧.
让我们从方法1的问题开始:
问题:你正在非正规化以节省速度.
在SQL中(除了带有hstore的PostGreSQL),你不能传递参数语言,并说:
SELECT ['DESCRIPTION_' + @in_language] FROM T_Products
Run Code Online (Sandbox Code Playgroud)
所以你必须这样做:
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
Run Code Online (Sandbox Code Playgroud)
这意味着如果添加新语言,则必须更改所有查询.这自然导致使用"动态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
Run Code Online (Sandbox Code Playgroud)
这个的问题是
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,但是将identity-insert设置为OFF ],这将很快(并且将)导致数据垃圾(和空引用异常).
-- 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&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
Run Code Online (Sandbox Code Playgroud)
然后你可以通过SQL中的XPath-Query获取值,你可以在其中放入字符串变量
filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla
Run Code Online (Sandbox Code Playgroud)
你可以像这样更新值:
UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with ""I am a ''value ""')
WHERE id = 1
Run Code Online (Sandbox Code Playgroud)
你可以替换/lang/de/...的地方'.../' + @in_language + '/...'
有点像PostGre hstore,除了由于解析XML的开销(而不是从PG hstore中的关联数组中读取条目),它变得太慢了加上xml编码使得它太痛苦而无法使用.
让我们举一个例子来看看这个工作:
首先,创建表:
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
Run Code Online (Sandbox Code Playgroud)
然后填写数据
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
Run Code Online (Sandbox Code Playgroud)
然后查询数据:
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
Run Code Online (Sandbox Code Playgroud)
如果你很懒,那么你也可以使用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设计更烦人,更具破坏性和麻烦.
而且,如果你仍然说"我们" 只是申请"只有一种文化"(通常是en-US) - 因此我不需要那个额外的整数,这将是一个很好的时间和地点来提及IANA语言标签,不是吗?
因为它们是这样的:
de-DE-1901
de-DE-1996
Run Code Online (Sandbox Code Playgroud)
和
de-CH-1901
de-CH-1996
Run Code Online (Sandbox Code Playgroud)
(1996年有一个拼写改革......)如果拼写错误,试着在字典中找一个单词; 这在处理法律和公共服务门户的应用程序中变得非常重要.
更重要的是,有些地区正在从西里尔字母改为拉丁字母,这可能比一些模糊的拼写改革的表面麻烦更麻烦,这也是为什么这可能是一个重要的考虑因素,这取决于你所居住的国家.不管怎样,最好在那里放一个整数,以防万一......
编辑:
并通过添加ON DELETE CASCADE 后
REFERENCES dbo.T_Products( PROD_Id )
Run Code Online (Sandbox Code Playgroud)
你可以简单地说:DELETE FROM T_Products,并且没有外键违规.
至于整理,我会这样做:
A)拥有自己的DAL
B)在语言表中保存所需的校对名称
您可能希望将排序规则放在自己的表中,例如:
SELECT * FROM sys.fn_helpcollations()
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%'
Run Code Online (Sandbox Code Playgroud)
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}
Run Code Online (Sandbox Code Playgroud)
E)然后,您可以在DAL中执行此操作:
cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)
Run Code Online (Sandbox Code Playgroud)
然后,这将为您提供这个完美组合的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
Run Code Online (Sandbox Code Playgroud)
Ada*_*vis 47
第三种选择是最好的,原因如下:
-亚当
小智 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)
)
Run Code Online (Sandbox Code Playgroud)
我认为没有必要解释,结构描述了自己.
我通常会采用这种方法(不是实际的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'
Run Code Online (Sandbox Code Playgroud)
因为在一个地方拥有所有可翻译的文本使得维护变得更加容易.有时翻译会外包给翻译机构,这样您就可以向他们发送一个大的导出文件,并轻松地将其导回.