Schema für eine mehrsprachige Datenbank

235

Ich entwickle eine mehrsprachige Software. In Bezug auf den Anwendungscode ist die Lokalisierbarkeit kein Problem. Wir können sprachspezifische Ressourcen verwenden und verfügen über alle Arten von Tools, die gut mit ihnen zusammenarbeiten.

Aber was ist der beste Ansatz bei der Definition eines mehrsprachigen Datenbankschemas? Angenommen, wir haben viele Tabellen (100 oder mehr), und jede Tabelle kann mehrere Spalten enthalten, die lokalisiert werden können (die meisten nvarchar-Spalten sollten lokalisierbar sein). Beispielsweise kann eine der Tabellen Produktinformationen enthalten:

CREATE TABLE T_PRODUCT (
  NAME        NVARCHAR(50),
  DESCRIPTION NTEXT,
  PRICE       NUMBER(18, 2)
)

Ich kann mir drei Ansätze vorstellen, um mehrsprachigen Text in den Spalten NAME und DESCRIPTION zu unterstützen:

  1. Separate Spalte für jede Sprache

    Wenn wir dem System eine neue Sprache hinzufügen, müssen wir zusätzliche Spalten erstellen, um den übersetzten Text wie folgt zu speichern:

    CREATE TABLE T_PRODUCT (
      NAME_EN        NVARCHAR(50),
      NAME_DE        NVARCHAR(50),
      NAME_SP        NVARCHAR(50),
      DESCRIPTION_EN NTEXT,
      DESCRIPTION_DE NTEXT,
      DESCRIPTION_SP NTEXT,
      PRICE          NUMBER(18,2)
    )
  2. Übersetzungstabelle mit Spalten für jede Sprache

    Anstatt übersetzten Text zu speichern, wird nur ein Fremdschlüssel für die Übersetzungstabelle gespeichert. Die Übersetzungstabelle enthält eine Spalte für jede Sprache.

    CREATE TABLE T_PRODUCT (
      NAME_FK        int,
      DESCRIPTION_FK int,
      PRICE          NUMBER(18, 2)
    )
    
    CREATE TABLE T_TRANSLATION (
      TRANSLATION_ID,
      TEXT_EN NTEXT,
      TEXT_DE NTEXT,
      TEXT_SP NTEXT
    )
  3. Übersetzungstabellen mit Zeilen für jede Sprache

    Anstatt übersetzten Text zu speichern, wird nur ein Fremdschlüssel für die Übersetzungstabelle gespeichert. Die Übersetzungstabelle enthält nur einen Schlüssel, und eine separate Tabelle enthält eine Zeile für jede Übersetzung in eine Sprache.

    CREATE TABLE T_PRODUCT (
      NAME_FK        int,
      DESCRIPTION_FK int,
      PRICE          NUMBER(18, 2)
    )
    
    CREATE TABLE T_TRANSLATION (
      TRANSLATION_ID
    )
    
    CREATE TABLE T_TRANSLATION_ENTRY (
      TRANSLATION_FK,
      LANGUAGE_FK,
      TRANSLATED_TEXT NTEXT
    )
    
    CREATE TABLE T_TRANSLATION_LANGUAGE (
      LANGUAGE_ID,
      LANGUAGE_CODE CHAR(2)
    )

Jede Lösung hat Vor- und Nachteile, und ich würde gerne wissen, welche Erfahrungen Sie mit diesen Ansätzen gemacht haben, was Sie empfehlen und wie Sie ein mehrsprachiges Datenbankschema entwerfen würden.

qbeuek
quelle
3
Sie können diesen Link überprüfen: gsdesign.ro/blog/multilanguage-database-design-approach, obwohl das Lesen der Kommentare sehr hilfreich ist
Fareed Alnamrouti
3
LANGUAGE_CODEsind natürliche Schlüssel, vermeiden LANGUAGE_ID.
Gavenkoa
1
Ich habe die 2. und 3. bereits gesehen / verwendet. Ich empfehle sie nicht. Sie haben leicht verwaiste Zeilen. @ SunWiKung Design sieht IMO besser aus.
Guillaume86
4
Ich bevorzuge das SunWuKungs-Design, das wir zufällig implementiert haben. Sie müssen jedoch Kollatierungen berücksichtigen. Zumindest in SQL Server verfügt jede Spalte über eine Sortierungseigenschaft, die Dinge wie Groß- und Kleinschreibung, Äquivalenz (oder nicht) von Akzentzeichen und andere sprachspezifische Überlegungen bestimmt. Ob Sie sprachspezifische Kollatierungen verwenden oder nicht, hängt von Ihrem gesamten Anwendungsdesign ab. Wenn Sie jedoch etwas falsch machen, können Sie es später nur schwer ändern. Wenn Sie sprachspezifische Kollatierungen benötigen, benötigen Sie eine Spalte pro Sprache, keine Zeile pro Sprache.
Elroy Flynn

Antworten:

113

Was halten Sie von einer zugehörigen Übersetzungstabelle für jede übersetzbare Tabelle?

CREATE TABLE T_PRODUCT (pr_id int, PREISNUMMER (18, 2))

CREATE TABLE T_PRODUCT_tr (pr_id INT FK, Sprachcode varchar, pr_name text, pr_descr text)

Wenn Sie über mehrere übersetzbare Spalten verfügen, ist nur ein einziger Join erforderlich, um diese + zu erhalten. Da Sie eine Übersetzungs-ID nicht automatisch generieren, ist es möglicherweise einfacher, Elemente zusammen mit den zugehörigen Übersetzungen zu importieren.

Die negative Seite davon ist, dass Sie bei einem komplexen Sprach-Fallback-Mechanismus diesen möglicherweise für jede Übersetzungstabelle implementieren müssen - wenn Sie sich dazu auf eine gespeicherte Prozedur verlassen. Wenn Sie dies über die App tun, ist dies wahrscheinlich kein Problem.

Lassen Sie mich wissen, was Sie denken - ich bin auch dabei, dies für unsere nächste Bewerbung zu entscheiden. Bisher haben wir Ihren 3. Typ verwendet.

Gemeinschaft
quelle
2
Diese Option ähnelt meiner Option Nr. 1, ist aber besser. Es ist immer noch schwer zu warten und erfordert das Erstellen neuer Tabellen für neue Sprachen. Daher würde ich es nur ungern implementieren.
Qbeuek
28
Für eine neue Sprache ist keine neue Tabelle erforderlich. Sie fügen der entsprechenden _tr-Tabelle einfach eine neue Zeile mit Ihrer neuen Sprache hinzu. Sie müssen nur dann eine neue _tr-Tabelle erstellen, wenn Sie eine neue übersetzbare Tabelle erstellen
3
Ich glaube, dass dies eine gute Methode ist. Andere Methoden erfordern Tonnen von Linksverknüpfungen. Wenn Sie mehrere Tabellen verbinden, von denen jede eine Übersetzung mit einer Tiefe von 3 Ebenen hat und jede 3 Felder hat, benötigen Sie 3 * 3 9 Linksverknüpfungen nur für Übersetzungen. Andernfalls 3. Auch dies Es ist einfacher, Einschränkungen usw. hinzuzufügen, und ich glaube, die Suche ist vernünftiger.
GorillaApe
1
Wann T_PRODUCThat 1 Million Zeilen, T_PRODUCT_trhätte 2 Millionen. Würde es die SQL-Effizienz stark reduzieren?
Mithril
1
@Mithril So oder so haben Sie 2 Millionen Zeilen. Zumindest brauchen Sie keine Verknüpfungen mit dieser Methode.
David D
56

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&amp;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 "&quot;I am a ''value &quot;"')
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
Stefan Steiger
quelle
Gute detaillierte Antwort, vielen Dank. Aber was halten Sie von den Kollatierungsproblemen in der Methode 5-Lösung? Es scheint, dass dies nicht der beste Weg ist, wenn Sie den übersetzten Text in der mehrsprachigen Umgebung mit verschiedenen Sortierungen sortieren oder filtern müssen. In diesem Fall könnte die Methode 2 (die Sie so schnell "geächtet" haben :)) eine bessere Option sein, da geringfügige Änderungen die Zielsortierung für jede lokalisierte Spalte anzeigen.
Eugene Evdokimov
2
@Eugene Evdokimov: Ja, aber "ORDER BY" wird immer ein Problem sein, da Sie es nicht als Variable angeben können. Mein Ansatz wäre, den Kollatierungsnamen in der Sprachtabelle zu speichern und diesen in der Benutzerinfo zu haben. Dann können Sie in jeder SQL-Anweisung ORDER BY COLUMN_NAME {#collation} sagen und dann in Ihrem dal (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user) ersetzen. language.collation). Alternativ können Sie Ihren Anwendungscode sortieren, z. B. mit LINQ. Dies würde auch Ihre Datenbank entlasten. Bei Berichten wird der Bericht trotzdem sortiert.
Stefan Steiger
oo Dies muss die längste SO-Antwort sein, die ich gesehen habe, und ich habe gesehen, wie Leute ganze Programme in Antworten erstellt haben. Du bist gut.
Domino
Kann völlig zustimmen, dass SunWuKungs Lösung die beste ist
Domi
48

Die dritte Option ist aus mehreren Gründen die beste:

  • Es ist nicht erforderlich, das Datenbankschema für neue Sprachen zu ändern (und damit Codeänderungen einzuschränken).
  • Benötigt nicht viel Platz für nicht implementierte Sprachen oder Übersetzungen eines bestimmten Elements
  • Bietet die größte Flexibilität
  • Sie haben keine spärlichen Tabellen
  • Sie müssen sich nicht um Nullschlüssel kümmern und überprüfen, ob Sie eine vorhandene Übersetzung anstelle eines Nulleintrags anzeigen.
  • Wenn Sie Ihre Datenbank ändern oder erweitern, um andere übersetzbare Elemente / Dinge / usw. einzuschließen, können Sie dieselben Tabellen und dasselbe System verwenden - dies ist sehr entkoppelt von den übrigen Daten.

-Adam

Adam Davis
quelle
1
Ich bin damit einverstanden, obwohl ich persönlich eine lokalisierte Tabelle für jede Haupttabelle hätte, damit Fremdschlüssel implementiert werden können.
Neil Barnwell
1
Obwohl die dritte Option die sauberste und fundierteste Implementierung des Problems ist, ist sie komplexer als die erste. Ich denke, das Anzeigen, Bearbeiten und Berichten der allgemeinen Version erfordert so viel zusätzlichen Aufwand, dass dies nicht immer akzeptabel ist. Ich habe beide Lösungen implementiert, die einfachere war genug, als die Benutzer eine schreibgeschützte (manchmal fehlende) Übersetzung der "Haupt" -Anwendungssprache benötigten.
Rics
12
Was ist, wenn die Produkttabelle mehrere übersetzte Felder enthält? Beim Abrufen von Produkten müssen Sie einen zusätzlichen Join pro übersetztem Feld ausführen, was zu schwerwiegenden Leistungsproblemen führt. Es gibt auch (IMO) zusätzliche Komplexität für das Einfügen / Aktualisieren / Löschen. Der einzige Vorteil davon ist die geringere Anzahl von Tabellen. Ich würde mich für die von SunWuKung vorgeschlagene Methode entscheiden: Ich denke, es ist ein gutes Gleichgewicht zwischen Leistung, Komplexität und Wartungsproblemen.
Frosty Z
@ rics- Ich stimme zu, na was schlägst du vor ...?
Säbel
@ Adam- Ich bin verwirrt, vielleicht habe ich falsch verstanden. Sie haben den dritten vorgeschlagen, oder? Bitte erläutern Sie es genauer, wie die Beziehungen zwischen diesen Tabellen aussehen werden. Sie meinen, wir müssen Translation- und TranslationEntry-Tabellen für jede Tabelle in DB implementieren?
Säbel
9

Schauen Sie sich dieses Beispiel an:

PRODUCTS (
    id   
    price
    created_at
)

LANGUAGES (
    id   
    title
)

TRANSLATIONS (
    id           (// id of translation, UNIQUE)
    language_id  (// id of desired language)
    table_name   (// any table, in this case PRODUCTS)
    item_id      (// id of item in PRODUCTS)
    field_name   (// fields to be translated)
    translation  (// translation text goes here)
)

Ich denke, es gibt keinen Grund zu erklären, die Struktur beschreibt sich selbst.

Bamburik
quelle
das ist gut. aber wie würden Sie suchen (zum Beispiel Produktname)?
Illuminati
Hatten Sie irgendwo ein Live-Beispiel Ihrer Probe? Haben Sie Probleme damit bekommen?
David Létourneau
Klar, ich habe ein mehrsprachiges Immobilienprojekt, wir unterstützen 4 Sprachen. Die Suche ist etwas kompliziert, aber schnell. Natürlich kann es bei großen Projekten langsamer sein, als es sein muss. In kleinen oder mittleren Projekten ist es ok.
Bamburik
8

Normalerweise würde ich diesen Ansatz wählen (nicht die tatsächliche SQL), dies entspricht Ihrer letzten Option.

table Product
productid INT PK, price DECIMAL, translationid INT FK

table Translation
translationid INT PK

table TranslationItem
translationitemid INT PK, translationid INT FK, text VARCHAR, languagecode CHAR(2)

view ProductView
select * from Product
inner join Translation
inner join TranslationItem
where languagecode='en'

Weil alle übersetzbaren Texte an einem Ort sind, was die Wartung so viel einfacher macht. Manchmal werden Übersetzungen an Übersetzungsbüros ausgelagert. Auf diese Weise können Sie ihnen nur eine große Exportdatei senden und sie genauso einfach zurück importieren.

user39603
quelle
1
Welchen Zweck erfüllt die TranslationTabelle oder die TranslationItem.translationitemidSpalte?
DanMan
4

Bevor Sie zu technischen Details und Lösungen gehen, sollten Sie eine Minute innehalten und einige Fragen zu den Anforderungen stellen. Die Antworten können einen großen Einfluss auf die technische Lösung haben. Beispiele für solche Fragen wären:
- Werden alle Sprachen ständig verwendet?
- Wer und wann füllt die Spalten mit den verschiedenen Sprachversionen?
- Was passiert, wenn ein Benutzer eine bestimmte Sprache eines Textes benötigt und keine im System vorhanden ist?
- Nur die Texte sollen lokalisiert werden oder es gibt auch andere Elemente (zum Beispiel kann PREIS in $ und € gespeichert werden, weil sie unterschiedlich sein können)

Aleris
quelle
Ich weiß, dass die Lokalisierung ein viel umfassenderes Thema ist, und ich bin mir der Probleme bewusst, auf die Sie mich aufmerksam machen, aber derzeit suche ich nach einer Antwort auf ein sehr spezifisches Problem des Schemadesigns. Ich gehe davon aus, dass neue Sprachen schrittweise hinzugefügt und fast vollständig übersetzt werden.
Qbeuek
3

Ich habe nach Tipps zur Lokalisierung gesucht und dieses Thema gefunden. Ich habe mich gefragt, warum dies verwendet wird:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Sie erhalten also so etwas wie user39603 vorschlägt:

table Product
productid INT PK, price DECIMAL, translationid INT FK

table Translation
translationid INT PK

table TranslationItem
translationitemid INT PK, translationid INT FK, text VARCHAR, languagecode CHAR(2)

view ProductView
select * from Product
inner join Translation
inner join TranslationItem
where languagecode='en'

Kannst du nicht einfach die Tabelle weglassen?

    table Product
    productid INT PK, price DECIMAL

    table ProductItem
    productitemid INT PK, productid INT FK, text VARCHAR, languagecode CHAR(2)

    view ProductView
    select * from Product
    inner join ProductItem
    where languagecode='en'
Randomizer
quelle
1
Sicher. Ich würde den ProductItemTisch so ProductTextsoder so nennen ProductL10n. Macht mehr Sinn.
DanMan
1

Ich stimme dem Randomizer zu. Ich verstehe nicht, warum Sie eine Tabelle "Übersetzung" benötigen.

Ich denke, das ist genug:

TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName
Bart VW
quelle
1

Wäre der folgende Ansatz praktikabel? Angenommen, Sie haben Tabellen, in denen mehr als eine Spalte übersetzt werden muss. Für das Produkt können Sie also sowohl den Produktnamen als auch die Produktbeschreibung haben, die übersetzt werden müssen. Könnten Sie Folgendes tun:

CREATE TABLE translation_entry (
      translation_id        int,
      language_id           int,
      table_name            nvarchar(200),
      table_column_name     nvarchar(200),
      table_row_id          bigint,
      translated_text       ntext
    )

    CREATE TABLE translation_language (
      id int,
      language_code CHAR(2)
    )   
Davey
quelle
0

"Welches ist das Beste" basiert auf der Projektsituation. Die erste ist einfach auszuwählen und zu warten, und auch die Leistung ist am besten, da bei der Auswahl der Entität keine Tabellen verknüpft werden müssen. Wenn Sie bestätigt haben, dass Ihr Projekt nur 2 oder 3 Sprachen unterstützt und es nicht zunimmt, können Sie es verwenden.

Der zweite ist ok, aber schwer zu verstehen und zu pflegen. Und die Leistung ist schlechter als die erste.

Der letzte ist gut skalierbar, aber schlecht in der Leistung. Die Tabelle T_TRANSLATION_ENTRY wird immer größer. Es ist schrecklich, wenn Sie eine Liste von Entitäten aus einigen Tabellen abrufen möchten.

studyzy
quelle
0

Dieses Dokument beschreibt die möglichen Lösungen sowie die Vor- und Nachteile der einzelnen Methoden. Ich bevorzuge die "Zeilenlokalisierung", da Sie das DB-Schema beim Hinzufügen einer neuen Sprache nicht ändern müssen.

Jaska
quelle