これは興味深い問題なので、ネクロマンスしましょう。
方法 1 の問題から始めましょう:
問題: 速度を節約するために非正規化しています。
SQL (hstore を使用する PostGreSQL を除く) では、パラメーター language を渡して次のように言うことはできません。
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 つのテーブルにまとめられるため、メンテナンスが簡単になります。ただし、同じことは、方法 1 と動的 SQL ストアド プロシージャ、および翻訳を含む (おそらく一時的な) テーブルと、ターゲット テーブルの名前を使用して実現できます (すべてのテキスト フィールドに同じ)。
方法 3:
すべての翻訳に対して 1 つのテーブル: 欠点: 翻訳する n フィールドの製品テーブルに n 外部キーを格納する必要があります。したがって、n 個のフィールドに対して n 個の結合を行う必要があります。変換テーブルがグローバルの場合、多くのエントリがあり、結合が遅くなります。また、常に T_TRANSLATION テーブルを n フィールドに対して 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 に似ていますが、(PG hstore の連想配列からエントリを読み取るのではなく) XML を解析するオーバーヘッドが原因で、処理が非常に遅くなり、xml エンコーディングによって使い物にならなくなる点が異なります。
方法 5 (SunWuKung が推奨する方法で、選択する必要があります): 各「製品」テーブルに対して 1 つの変換テーブル。これは、言語ごとに 1 つの行と複数の「テキスト」フィールドを意味するため、N フィールドに対して 1 つの (左) 結合のみが必要です。次に、「製品」テーブルにデフォルト フィールドを簡単に追加し、変換テーブルを簡単に削除して再挿入し、カスタム翻訳用の 2 つ目のテーブルを (オンデマンドで) 作成して、これを削除することもできます。再挿入)、すべての外部キーがまだあります。
これが機能することを確認する例を作成しましょう。
まず、テーブルを作成します。
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 を取得するため、正書法の観点からは実際には同じではない (どこでも ß の代わりに 2 つの s がある) ためです。 、同じ基本言語ですが。特に en-US と en-GB/en-CA/en-AU または fr-FR/fr-CA が同様の問題を抱えていることを考えると、それはあなたにとって重要かもしれない小さな詳細です。
引用: 私たちはそれを必要としません。私たちはソフトウェアを英語でのみ行っています。
回答: はい - しかしどれですか??
いずれにせよ、整数 ID を使用する場合は柔軟であり、後でいつでもメソッドを変更できます。
そして、その整数を使用する必要があります。なぜなら、失敗した Db 設計ほど厄介で、破壊的で、厄介なものはないからです。
RFC 5646、ISO 639-2も参照してください。
また、「私たち」は「1 つのカルチャのみ」(通常は 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