Это интересный вопрос, так что давайте разберемся.
Давайте начнем с задач метода 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>
изменяет схему из-за внезапного изменения требований к крылу, и создается новая таблица, старая остается, но поле ссылки переименовывается, вы не получаете никаких предупреждений. Отчет даже работает , когда вы его запускаете, не выбрав параметр wing (==> guid.empty). Но неожиданно, когда реальный пользователь фактически выбирает крыло ==> boom . Этот метод полностью нарушает любой вид тестирования.
<ч />
Метод 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 () [или указав идентификатор в вставке, но имея identity-insert OFF ], и это приведет (и приведет) к мусору данных (и исключениям нулевой ссылки) очень скоро.
<ч />
Способ 4 (не указан):
Хранение всех языков в поле XML в базе данных.
* 1038 например *
-- 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 полей.
Затем вы можете легко добавить поле по умолчанию в «Product» -таблицу, вы можете легко удалить и повторно вставить таблицу перевода, и вы можете создать вторую таблицу для пользовательских переводов (по запросу), которую вы также можете удалить и вставьте заново), и у вас все еще есть все внешние ключи.
Давайте сделаем пример, чтобы увидеть это РАБОТЫ:
Сначала создайте таблицы:
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-language , что лучше, потому что вы получаете 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
и не получить никакого нарушения внешнего ключа.
Что касается сортировки, я бы сделал это так:
А) Имейте свой собственный 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}
Д) Затем вы можете сделать это в своем 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