Це цікаве питання, тож давайте некромантизувати.
Почнемо з проблем методу 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
Проблема в цьому полягає
в тому, що) Форматування дати дуже специфічне для мови, тому у вас виникає проблема, якщо ви не вводите у форматі ISO (що зазвичай не робить звичайний програміст для садових сортів, і у випадку звіту, користувач впевнений, що пекло не зробить вас, навіть якщо це чітко доручено зробити це).
і
б) найголовніше , що ви втрачаєте будь-яку перевірку синтаксису . Якщо <insert name of your "favourite" person here>
схема змінюється через те, що раптом змінюються вимоги до крила, і створюється нова таблиця, стара залишається, але посилання перейменовано, а ви не отримуєте жодного попередження. Звіт працює навіть при його запуску, не вибираючи параметр крила (==> guide.empty). Але раптом, коли фактично користувач фактично вибирає крило ==>бум . Цей метод повністю порушує будь-який вид тестування.
Спосіб 2:
Коротше кажучи: «Чудова» ідея (попередження - сарказм), давайте поєднаємо недоліки методу 3 (повільна швидкість, коли багато записів) із досить жахливими недоліками методу 1.
Єдиною перевагою цього методу є те, що ви дотримуєтесь весь переклад в одній таблиці, а тому робить технічне обслуговування простим. Однак те ж саме можна досягти з методом 1 та динамічною процедурою, що зберігається в SQL, та (можливо, тимчасовою) таблицею, що містить переклади та ім’я цільової таблиці (і це досить просто, якщо ви назвали всі свої текстові поля те саме).
Спосіб 3:
Одна таблиця для всіх перекладів: Недолік: Ви повинні зберігати n іноземних ключів у таблиці продуктів для n полів, які ви хочете перекласти. Тому ви повинні зробити n приєднань для n полів. Коли таблиця перекладів є глобальною, вона містить багато записів, і з'єднання стають повільними. Крім того, ви завжди повинні приєднатись до таблиці T_TRANSLATION n разів для n полів. Це цілком накладні витрати. Тепер, що ви робите, коли ви повинні вмістити власні переклади на кожного клієнта? Вам доведеться додати ще 2x n приєднань до додаткової таблиці. Якщо вам доведеться приєднатися, скажімо, 10 таблиць, з додатковими приєднаннями 2x2xn = 4n, який безлад! Також ця конструкція дає можливість використовувати один і той же переклад з 2 таблицями. Якщо я зміню ім'я елемента в одній таблиці, чи дійсно я хочу змінити запис в іншій таблиці, ВСЕ ОДНІЙ ЧАС?
Крім того, ви більше не можете видаляти та повторно вставляти таблицю, тому що в таблиці СТОКІВ ПРОДУКТУ зараз є сторонні ключі ... Ви, звичайно, можете опустити налаштування FK, а потім <insert name of your "favourite" person here>
можна видалити таблицю та повторно вставити всі записи з newid () [або шляхом введення ідентифікатора у вкладиші, але мають ідентифікатор-вставку 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
Тоді ви можете отримати значення за 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 + '/...'
Наче подібний hstore PostGre, за винятком того, що через накладні розбори 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, що насправді не є одинаковим для ортографії (подвійний s замість ß скрізь) , хоча це та сама основна мова. Це як маленька маленька деталь, яка може бути важливою для вас, особливо враховуючи, що en-US і en-GB / en-CA / en-AU або fr-FR / fr-CA мають подібні проблеми.
Цитата: нам це не потрібно, ми робимо лише своє програмне забезпечення англійською мовою.
Відповідь: Так - але який із них ??
У будь-якому випадку, якщо ви використовуєте цілий ідентифікатор, ви гнучкі і можете змінити свій метод у будь-який пізній час.
І вам слід використовувати це ціле число, тому що немає нічого більш дратівливого, руйнівного та клопіткого, ніж боб-дизайн 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