นี่เป็นปัญหาที่น่าสนใจดังนั้นมาลองเรื่องลึกลับกันเถอะ
มาเริ่มจากปัญหาของวิธีที่ 1:
ปัญหา: คุณกำลังลดความเร็วในการบันทึกลง
ใน SQL (ยกเว้น PostGreSQL ด้วย hstore) คุณไม่สามารถผ่านภาษาพารามิเตอร์และพูดว่า:
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 (ซึ่งโปรแกรมเมอร์ทั่วไปที่มีความหลากหลายในสวนโดยทั่วไปจะไม่ทำและในกรณีของ รายงานผู้ใช้ว่าเป็นนรกจะไม่ทำเพื่อคุณแม้ว่าจะได้รับคำแนะนำให้ทำอย่างชัดเจน)
และ
ข) ความหมายมากที่สุดคุณหลวมชนิดของไวยากรณ์การตรวจสอบใด ๆ หาก<insert name of your "favourite" person here>
เปลี่ยนแปลง schema เพราะทันใดความต้องการการเปลี่ยนปีกและสร้างตารางใหม่ตารางเก่าจะถูกทิ้ง แต่ฟิลด์การอ้างอิงเปลี่ยนชื่อคุณจะไม่ได้รับคำเตือนใด ๆ รายงานยังใช้งานได้เมื่อคุณรันโดยไม่เลือกพารามิเตอร์ wing (==> guid.empty) แต่ทันใดนั้นเมื่อผู้ใช้จริงเลือก wing ==>ความเจริญ วิธีนี้จะทำการทดสอบทุกรูปแบบอย่างสมบูรณ์
วิธีที่ 2:
สรุป: ความคิด "ยอดเยี่ยม" (คำเตือน - การประชดประชัน) เรามารวมข้อเสียของวิธีที่ 3 (ความเร็วช้าเมื่อมีหลายรายการ) เข้ากับข้อเสียที่ค่อนข้างน่ากลัวของวิธีที่ 1
ข้อดีเพียงอย่างเดียวของวิธีนี้คือ การแปลทั้งหมดในตารางเดียวและทำให้บำรุงรักษาง่าย อย่างไรก็ตามสิ่งเดียวกันสามารถเกิดขึ้นได้ด้วยวิธีที่ 1 และขั้นตอนการจัดเก็บ SQL แบบไดนามิกและตาราง (อาจชั่วคราว) ที่มีการแปลและชื่อของตารางเป้าหมาย (และค่อนข้างง่ายสมมติว่าคุณตั้งชื่อฟิลด์ข้อความทั้งหมดของคุณ เหมือนกัน).
วิธีที่ 3:
ตารางหนึ่งสำหรับการแปลทั้งหมด: ข้อเสีย: คุณต้องเก็บ n Foreign Keys ในตารางผลิตภัณฑ์สำหรับ n field ที่คุณต้องการแปล ดังนั้นคุณต้องทำ n รวมสำหรับฟิลด์ n เมื่อตารางการแปลเป็นสากลมันมีหลายรายการและเข้าร่วมช้า นอกจากนี้คุณต้องเข้าร่วม T_TRANSLATION ตาราง n ครั้งสำหรับฟิลด์ n นี่เป็นค่าโสหุ้ย ตอนนี้คุณจะทำอย่างไรเมื่อคุณต้องรองรับการแปลที่กำหนดเองต่อลูกค้า? คุณจะต้องเพิ่ม 2xn รวมเข้ากับตารางเพิ่มเติม ถ้าคุณต้องเข้าร่วมพูดถึง 10 โต๊ะโดยมีการเข้าร่วมเพิ่มเติม 2x2xn = 4n มันช่างเป็นระเบียบ! นอกจากนี้การออกแบบนี้ทำให้สามารถใช้การแปลเดียวกันกับ 2 ตาราง ถ้าฉันเปลี่ยนชื่อรายการในหนึ่งตารางฉันต้องการเปลี่ยนรายการในตารางอื่นด้วยเช่นกันทุกครั้งหรือไม่
ยิ่งไปกว่านั้นคุณไม่สามารถลบและแทรกตารางอีกต่อไปได้เนื่องจากขณะนี้มีคีย์ต่างประเทศในตารางผลิตภัณฑ์ ... คุณสามารถละเว้นการตั้งค่า FK จากนั้น<insert name of your "favourite" person here>
สามารถลบตารางและแทรกใหม่ได้ รายการทั้งหมดที่มีnewid () [หรือโดยการระบุรหัสในส่วนแทรก แต่มีการปิดการใส่ข้อมูลประจำตัว ] และสิ่งนั้นจะนำไปสู่ข้อมูลขยะ (และข้อยกเว้นอ้างอิงโมฆะ) ในไม่ช้า
วิธีที่ 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
จากนั้นคุณสามารถรับค่าโดย XPath-Query ใน SQL ซึ่งคุณสามารถใส่ตัวแปรสตริงได้
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' ฯลฯ ) เป็นคีย์หลักของตารางภาษาได้ดังนั้นคุณไม่จำเป็นต้องค้นหารหัสภาษา แต่ถ้าคุณทำเช่นนั้นคุณอาจต้องการใช้แท็กภาษา IETFแทนซึ่งดีกว่าเพราะคุณได้รับ de-CH และ de-DE ซึ่งจริงๆแล้วไม่ใช่ ortography-wise เดียวกัน (double s แทนßทุกที่) แม้ว่ามันจะเป็นภาษาฐานเดียวกัน นั่นเป็นเพียงรายละเอียดเล็ก ๆ น้อย ๆ ที่อาจสำคัญกับคุณโดยเฉพาะเมื่อพิจารณาว่า en-US และ en-GB / en-CA / en-AU หรือ fr-FR / fr-CA มีปัญหาที่คล้ายกัน
อ้างอิง: เราไม่ต้องการเราทำแค่ซอฟต์แวร์เป็นภาษาอังกฤษเท่านั้น
คำตอบ: ใช่ - แต่อันไหน ??
อย่างไรก็ตามหากคุณใช้ 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-Query อย่างสมบูรณ์
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE German_PhoneBook_CI_AI