这是一个有趣的问题,所以让我们使用死灵法。
让我们从方法 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 的需求突然改变而改变了模式,并创建了一个新表,旧表离开但引用字段重命名,您不会收到任何警告。当您在不选择wing 参数(==> guid.empty)的情况下运行报告时,它甚至可以工作。但是突然间,当实际用户实际选择了一个机翼时 ==>繁荣。这种方法完全打破了任何类型的测试。
方法2:
简而言之:“好”的想法(警告-讽刺),让我们将方法3的缺点(许多条目时速度慢)与方法1的相当可怕的缺点结合起来。
这种方法的唯一优点是你保持所有翻译都在一张表中,因此使维护变得简单。但是,使用方法 1 和动态 SQL 存储过程以及包含翻译的(可能是临时的)表以及目标表的名称(假设您将所有文本字段命名为相同的)。
方法3:
一张表,所有翻译:缺点:你必须在products表中为你要翻译的n个字段存储n个外键。因此,您必须对 n 个字段进行 n 次连接。当翻译表是全局的时,它有很多条目,并且连接变得很慢。此外,您总是必须为 n 个字段加入 T_TRANSLATION 表 n 次。这是相当大的开销。现在,当您必须为每位客户提供自定义翻译时,您会怎么做?您必须将另一个 2x n 连接添加到另一个表中。如果你必须加入,比如说 10 个表,还有 2x2xn = 4n 个额外的连接,那真是一团糟!此外,这种设计使得可以对 2 个表使用相同的翻译。如果我更改一个表中的项目名称,我真的想每次都更改另一个表中的条目吗?
另外,您不能再删除并重新插入表,因为现在产品表中有外键...您当然可以省略设置 FK,然后<insert name of your "favourite" person here>
可以删除表并重新插入所有带有newid() [或通过在插入中指定 id,但具有标识插入 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'<?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
然后您可以通过 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(根据孙悟空的推荐,你应该选择的那个):每个“产品”表对应一个翻译表。这意味着每种语言一行,以及几个“文本”字段,因此它只需要在 N 个字段上进行 ONE(左)连接。然后您可以轻松地在“产品”表中添加一个默认字段,您可以轻松删除并重新插入翻译表,您可以创建第二个自定义翻译表(按需),您也可以删除并重新插入),您仍然拥有所有外键。
让我们举个例子来看看这个作品:
首先,创建表:
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,这实际上是不一样的正字法(双 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