Dies ist ein interessantes Thema, also lasst uns nekromantieren.
Beginnen wir mit den Problemen von Methode 1:
Problem: Sie denormalisieren, um Geschwindigkeit zu sparen.
In SQL (außer PostGreSQL mit hstore) können Sie keine Parametersprache übergeben und sagen:
SELECT ['DESCRIPTION_' + @in_language] FROM T_Products
Also musst du das machen:
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
Das heißt, Sie müssen ALLE Ihre Abfragen ändern, wenn Sie eine neue Sprache hinzufügen. Dies führt natürlich zur Verwendung von "dynamischem SQL", sodass Sie nicht alle Ihre Abfragen ändern müssen.
Dies führt normalerweise zu so etwas (und kann übrigens nicht in Ansichten oder Funktionen mit Tabellenwerten verwendet werden, was wirklich ein Problem ist, wenn Sie das Berichtsdatum tatsächlich filtern müssen).
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
Das Problem dabei ist:
a) Die Datumsformatierung ist sehr sprachspezifisch, sodass Sie dort ein Problem erhalten, wenn Sie nicht im ISO-Format eingeben (was der durchschnittliche Programmierer für Gartensorten normalerweise nicht tut, und im Fall von Ein Bericht, den der Benutzer verdammt noch mal nicht für Sie tun wird, auch wenn er ausdrücklich dazu aufgefordert wird.
und
b) am wichtigsten ist , dass Sie jede Art von Syntaxprüfung verlieren . Wenn <insert name of your "favourite" person here>
sich das Schema ändert, weil sich plötzlich die Anforderungen für den Flügel ändern und eine neue Tabelle erstellt wird, die alte übrig bleibt, aber das Referenzfeld umbenannt wird, erhalten Sie keine Warnung. Ein Bericht funktioniert auch, wenn Sie ihn ausführen, ohne den Wing-Parameter (==> guid.empty) auszuwählen. Aber plötzlich, wenn ein tatsächlicher Benutzer tatsächlich einen Flügel auswählt ==>Boom . Diese Methode unterbricht jede Art von Test vollständig.
Methode 2:
Kurz gesagt: "Großartige" Idee (Warnung - Sarkasmus), kombinieren wir die Nachteile von Methode 3 (langsame Geschwindigkeit bei vielen Einträgen) mit den ziemlich schrecklichen Nachteilen von Methode 1.
Der einzige Vorteil dieser Methode besteht darin, dass Sie sie beibehalten Alle Übersetzungen in einer Tabelle und vereinfachen daher die Wartung. Dasselbe kann jedoch mit Methode 1 und einer gespeicherten dynamischen SQL-Prozedur sowie einer (möglicherweise temporären) Tabelle erreicht werden, die die Übersetzungen und den Namen der Zieltabelle enthält (und ist recht einfach, vorausgesetzt, Sie haben alle Ihre Textfelder mit benannt gleich).
Methode 3:
Eine Tabelle für alle Übersetzungen: Nachteil: Sie müssen n Fremdschlüssel in der Produkttabelle für n Felder speichern, die Sie übersetzen möchten. Daher müssen Sie n Verknüpfungen für n Felder ausführen. Wenn die Übersetzungstabelle global ist, enthält sie viele Einträge und Verknüpfungen werden langsam. Außerdem müssen Sie für n Felder immer n-mal der Tabelle T_TRANSLATION beitreten. Dies ist ein ziemlicher Aufwand. Was tun Sie nun, wenn Sie benutzerdefinierte Übersetzungen pro Kunde vornehmen müssen? Sie müssen weitere 2x n Verknüpfungen zu einer zusätzlichen Tabelle hinzufügen. Wenn Sie beitreten müssen, sagen wir 10 Tabellen, mit 2x2xn = 4n zusätzlichen Verknüpfungen, was für ein Durcheinander! Dieses Design ermöglicht es auch, dieselbe Übersetzung mit 2 Tabellen zu verwenden. Wenn ich den Elementnamen in einer Tabelle ändere, möchte ich dann wirklich JEDES MAL einen Eintrag in einer anderen Tabelle ändern?
Außerdem können Sie die Tabelle nicht mehr löschen und erneut einfügen, da es jetzt Fremdschlüssel in den Produkttabellen gibt. Sie können natürlich das Festlegen der FKs weglassen und dann <insert name of your "favourite" person here>
die Tabelle löschen und erneut einfügen alle Einträge mit newid () [oder durch Angabe der ID in der Einfügung, aber Identitätseinfügung ], und das würde (und wird) sehr bald ) führen.
Methode 4 (nicht aufgeführt): Speichern aller Sprachen in einem XML-Feld in der Datenbank. z.B
-- 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
Dann können Sie den Wert durch XPath-Query in SQL erhalten, wo Sie die String-Variable eingeben können
filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla
Und Sie können den Wert folgendermaßen aktualisieren:
UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with ""I am a ''value ""')
WHERE id = 1
Wo Sie ersetzen können /lang/de/...
mit'.../' + @in_language + '/...'
Ähnlich wie der PostGre-Hstore, nur dass er aufgrund des Overheads beim Parsen von XML (anstatt einen Eintrag aus einem assoziativen Array im PG-Hstore zu lesen) viel zu langsam wird und die XML-Codierung ihn zu schmerzhaft macht, um nützlich zu sein.
Methode 5 (wie von SunWuKung empfohlen, die Sie auswählen sollten): Eine Übersetzungstabelle für jede "Produkt" -Tabelle. Das bedeutet eine Zeile pro Sprache und mehrere "Text" -Felder, sodass nur EIN (linker) Join für N Felder erforderlich ist. Dann können Sie einfach ein Standardfeld in die "Produkt" -Tabelle einfügen, die Übersetzungstabelle einfach löschen und erneut einfügen und eine zweite Tabelle für benutzerdefinierte Übersetzungen (bei Bedarf) erstellen, die Sie auch löschen können und erneut einfügen), und Sie haben immer noch alle Fremdschlüssel.
Lassen Sie uns ein Beispiel machen, um diese WERKE zu sehen:
Erstellen Sie zunächst die Tabellen:
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
Dann geben Sie die Daten ein
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
Und dann die Daten abfragen:
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
Wenn Sie faul sind, können Sie auch den ISO-TwoLetterName ('DE', 'EN' usw.) als Primärschlüssel für die Sprachtabelle verwenden. Dann müssen Sie die Sprach-ID nicht nachschlagen. Aber wenn Sie dies tun, möchten Sie vielleicht stattdessen das IETF-Sprach-Tag verwenden, was besser ist, weil Sie de-CH und de-DE erhalten, was in Bezug auf die Ortographie wirklich nicht gleich ist (doppelte s statt ß überall). , obwohl es die gleiche Basissprache ist. Dies ist nur ein kleines Detail, das für Sie wichtig sein kann, insbesondere wenn man bedenkt, dass en-US und en-GB / en-CA / en-AU oder fr-FR / fr-CA ähnliche Probleme haben.
Quote: Wir brauchen es nicht, wir machen unsere Software nur auf Englisch.
Antwort: Ja - aber welches?
Wenn Sie eine Ganzzahl-ID verwenden, sind Sie auf jeden Fall flexibel und können Ihre Methode zu einem späteren Zeitpunkt ändern.
Und Sie sollten diese Ganzzahl verwenden, denn nichts ist ärgerlicher, destruktiver und problematischer als ein verpfuschtes Db-Design.
Siehe auch RFC 5646 , ISO 639-2 ,
Und wenn Sie immer noch sagen „wir“ nur unsere Anwendung machen für „nur eine Kultur“ (wie en-US in der Regel) - deshalb brauche ich nicht , dass zusätzliche integer, das ist eine gute Zeit und Ort wäre das zu erwähnen , IANA-Sprach-Tags , nicht wahr?
Weil sie so gehen:
de-DE-1901
de-DE-1996
und
de-CH-1901
de-CH-1996
(1996 gab es eine Rechtschreibreform ...) Versuchen Sie, ein Wort in einem Wörterbuch zu finden, wenn es falsch geschrieben ist. Dies wird sehr wichtig bei Anwendungen, die sich mit rechtlichen und öffentlich-rechtlichen Portalen befassen.
Noch wichtiger ist, dass es Regionen gibt, die von kyrillischen zu lateinischen Alphabeten wechseln. Dies ist möglicherweise problematischer als das oberflächliche Ärgernis einer obskuren Rechtschreibreform, weshalb dies je nach Land, in dem Sie leben, ebenfalls eine wichtige Überlegung sein kann. Auf die eine oder andere Weise ist es besser, diese ganze Zahl dort zu haben, nur für den Fall ...
Bearbeiten:
Und durch Hinzufügen ON DELETE CASCADE
nach
REFERENCES dbo.T_Products( PROD_Id )
man kann einfach sagen: DELETE FROM T_Products
und erhalten keine Fremdschlüsselverletzung.
Die Zusammenstellung würde ich folgendermaßen machen:
A) Haben Sie Ihren eigenen DAL.
B) Speichern Sie den gewünschten Sortiernamen in der Sprachtabelle
Möglicherweise möchten Sie die Kollatierungen in eine eigene Tabelle einfügen, z.
SELECT * FROM sys.fn_helpcollations()
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%'
C) Halten Sie den Kollatierungsnamen in Ihren auth.user.language-Informationen bereit
D) Schreiben Sie Ihre SQL wie folgt:
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE {#COLLATION}
E) Dann können Sie dies in Ihrem DAL tun:
cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)
Damit erhalten Sie diese perfekt zusammengestellte SQL-Abfrage
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE German_PhoneBook_CI_AI