这是一个有趣的问题,所以让我们开始讨论吧。
让我们从方法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:
所有翻译使用一个表:缺点:您必须在product表中存储要翻译的n个字段的n个外键。因此,您必须对n个字段进行n个联接。当转换表是全局表时,它有许多条目,并且连接变慢。同样,对于n个字段,您始终必须连接T_TRANSLATION表n次。这是相当大的开销。现在,当您必须为每个客户提供定制翻译时,您会怎么做?您必须在另一个表上添加另一个2x n个联接。如果必须联接,比如说10个表,还有2x2xn = 4n个附加联接,那真是一团糟!同样,这种设计使得可以对2个表使用相同的转换。如果我在一个表中更改了项目名称,我真的想同时在另一个表中更改条目吗?
另外,您不能再删除并重新插入表,因为产品表中现在有外键...您当然可以省略设置FK,然后<insert name of your "favourite" person here>
可以删除表并重新插入所有带有newid()的条目(或通过在插入中指定ID,但将identity-insert设置为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&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(SunWuKung建议,应选择一种方法):每个“产品”表都有一个转换表。这意味着每种语言一行,还有几个“文本”字段,因此在N个字段上只需要一个(左)联接。然后,您可以轻松地在“产品”表中添加默认字段,可以轻松地删除并重新插入翻译表,还可以创建第二个表用于自定义翻译(按需),也可以将其删除并重新插入),您仍然拥有所有外键。
让我们举个例子看看这个作品:
首先,创建表:
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