Schemat wielojęzycznej bazy danych

Pracuję nad oprogramowaniem wielojęzycznym. Jeśli chodzi o kod aplikacji, Lokalizacja nie stanowi problemu. Możemy korzystać z zasobów specyficznych dla języka i mieć wszelkiego rodzaju narzędzia, które dobrze z nimi współpracują.

Ale jakie jest najlepsze podejście do definiowania schematu wielojęzycznej bazy danych? Załóżmy, że mamy wiele tabel (100 lub więcej), a każda tabela może mieć wiele kolumn, które mogą być zlokalizowane (większość kolumn nvarchar powinna być możliwa do zlokalizowania). Na przykład jedna z tabel może przechowuj informacje o produkcie:

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

Mogę wymyślić trzy podejścia do obsługi wielojęzycznego tekstu w kolumnach z nazwą i opisem:

  1. Oddzielna kolumna dla każdego języka

    Kiedy dodajemy nowy język do systemu, musimy utworzyć dodatkowe kolumny do przechowywania przetłumaczonego tekstu, jak to:

    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. Tabela tłumaczeń z kolumnami dla każdego języka

    Zamiast przechowywać przetłumaczony tekst, tylko klucz obcy do tabeli tłumaczeń jest przechowywany. Tabela tłumaczeń zawiera kolumnę dla każdego języka.

    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. Tabele tłumaczeń z wierszami dla każdego języka

    Zamiast przechowywać przetłumaczony tekst, przechowywany jest tylko klucz obcy do tabeli tłumaczeń. Tabela tłumaczenia zawiera tylko klucz, a oddzielna tabela zawiera wiersz dla każdego tłumaczenia na język.

    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)
    )
    

Są plusy i minusy każdego rozwiązania, i chciałbym wiedzieć, jakie są Twoje doświadczenia z te podejścia, co polecacie i jak można przejść o projektowaniu wielojęzycznego schematu bazy danych.

Author: Stijn, 2008-11-25

10 answers

Co sądzisz o powiązanej tabeli tłumaczeniowej dla każdej tabeli tłumaczeniowej?

Utwórz tabelę T_PRODUCT(pr_id int, PRICE NUMBER (18, 2))

CREATE TABLE T_PRODUCT_TR (PR_ID INT FK, languagecode varchar, pr_name text, pr_descr text)

W ten sposób, jeśli masz wiele translatowalnych kolumn, wymagałoby to tylko jednego join, aby go uzyskać + ponieważ nie automatyzujesz translationid, może być łatwiej zaimportować elementy razem z ich powiązane tłumaczenia.

Negatywną stroną tego jest to, że jeśli masz złożony mechanizm rezerwowy języka, może być konieczne zaimplementowanie go dla każdej tabeli tłumaczeń - jeśli polegasz na jakiejś procedurze składowanej, aby to zrobić. Jeśli zrobisz to z aplikacji, prawdopodobnie nie będzie to problemem.

Daj mi znać, co myślisz - ja również zamierzam podjąć decyzję w tej sprawie do naszego następnego wniosku. Do tej pory użyliśmy twojego trzeciego typu.

 97
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-27 10:02:17

Trzecia opcja jest najlepsza, z kilku powodów:

  • nie wymaga zmiany schematu bazy danych dla nowych języków (a tym samym ograniczenia zmian w kodzie)
  • nie wymaga dużo miejsca na nie zaimplementowane języki lub tłumaczenia konkretnego elementu
  • zapewnia największą elastyczność
  • nie kończysz z rzadkimi tabelami
  • nie musisz się martwić o klucze null i sprawdzanie, czy wyświetlasz istniejące tłumaczenie zamiast jakiegoś null wejście.
  • Jeśli zmienisz lub rozszerzysz swoją bazę danych o inne możliwe do przetłumaczenia elementy/rzeczy / etc, możesz użyć tych samych tabel i systemu - jest to bardzo niezwiązane z resztą danych.

- Adam

 45
Author: Adam Davis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-25 14:36:16

To interesująca sprawa, więc zróbmy nekromancję.

Zacznijmy od problemów metody 1:
Problem: denormalizujesz, aby zaoszczędzić prędkość.
W SQL (z wyjątkiem PostGreSQL z hstore) nie można przekazać języka parametru i powiedzieć:

SELECT ['DESCRIPTION_' + @in_language]  FROM T_Products

Więc musisz to zrobić:

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 

Co oznacza, że musisz zmienić wszystkie zapytania, jeśli dodasz nowy język. Prowadzi to naturalnie do używania "dynamicznego SQL", więc nie musisz zmieniać wszystkich swoich zapytania.

Zazwyczaj skutkuje to czymś takim (i nie może być używane w widokach lub funkcjach o wartości tabeli, co naprawdę jest problemem, jeśli faktycznie musisz filtrować datę raportowania)

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

Problem z tym jest
a) formatowanie daty jest bardzo specyficzne dla języka, więc masz problem, jeśli nie wprowadzasz formatu ISO (czego Przeciętny programista garden-variety zwykle nie robi, a w przypadku raportu użytkownik na pewno nie zrobi za ty, nawet jeśli wyraźnie polecił to zrobić).
oraz
b) najbardziej istotne , ty luźne wszelkiego rodzaju sprawdzanie składni . Jeśli <insert name of your "favourite" person here> zmieni schemat, ponieważ nagle zmieniają się wymagania dotyczące skrzydła i powstaje nowa tabela, stara pozostała, ale zmieniono nazwę pola odniesienia, nie otrzymasz żadnego ostrzeżenia. Raport działa nawet , gdy go uruchamiasz bez wybierania parametru wing (==>guid.pusta). Ale nagle, gdy rzeczywisty użytkownik faktycznie wybiera skrzydło = = >boom . ta metoda całkowicie przerywa wszelkiego rodzaju testy.


Metoda 2:
W skrócie: "świetny" pomysł (Ostrzeżenie - sarkazm), połączmy wady metody 3 (powolna prędkość, gdy wiele wpisów) z raczej okropnymi wadami metody 1.
Jedyną zaletą tej metody jest to, że zachowujesz wszystkie tłumaczenia w jednej tabeli, a tym samym ułatwiasz konserwację. Jednak to samo można osiągnąć za pomocą metody 1 i a dynamiczna procedura składowana SQL oraz (być może tymczasowa) tabela zawierająca tłumaczenia i nazwę tabeli docelowej (i jest dość prosta, zakładając, że wszystkie pola tekstowe zostały nazwane tak samo).


Metoda 3:
Jedna tabela dla wszystkich tłumaczeń: Wada: W tabeli produktów należy zapisać n kluczy obcych dla n pól, które chcesz przetłumaczyć. W związku z tym, musisz zrobić N łączy dla n pól. Gdy tabela tłumaczeń jest globalna, ma wiele wpisów, a dołącza się powoli. Ponadto, zawsze musisz dołączyć do tabeli T_TRANSLATION N razy dla n pól. To spory wydatek. Teraz, co zrobić, gdy trzeba dostosować niestandardowe tłumaczenia na klienta ? Będziesz musiał dodać kolejne 2x N joins na dodatkowej tabeli. Jeśli musisz dołączyć, powiedzmy 10 tabel, z 2x2xn = 4N dodatkowe połączenia , co za bałagan ! Ponadto, ta konstrukcja umożliwia użycie tego samego tłumaczenia z 2 tabelami. Jeśli zmienię nazwę elementu w jednej tabeli, czy naprawdę chcę zmienić wpis do innej tabeli za każdym razem ?

Poza tym nie można już usuwać i wstawiać tabeli ponownie, ponieważ w tabeli produktów są teraz klucze obce... można oczywiście pominąć ustawianie FKs, a następnie {[16] } można usunąć tabelę i ponownie wstawić wszystkie wpisy za pomocą nevid () [lub przez podanie id w wstawce, ale mając identity-insert OFF ], co doprowadziłoby (i doprowadziłoby) do śmieci danych (i wyjątków od null) naprawdę szybko.


Metoda 4 (niewymieniona w czołówce)): Przechowywanie wszystkich języków w polu XML w bazie danych. np.
-- 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 

Następnie możesz uzyskać wartość za pomocą XPath-Query w SQL, gdzie możesz umieścić zmienną string w

filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla

I możesz zaktualizować wartość w następujący sposób:

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 

Gdzie można zastąpić /lang/de/... '.../' + @in_language + '/...'

Trochę jak PostGre hstore, tyle że ze względu na narzut parsowania XML (zamiast odczytu wpisu z tablicy asocjacyjnej w PG hstore) staje się zbyt wolny, a kodowanie xml sprawia, że jest zbyt bolesne, aby być użytecznym.


Metoda 5 (zgodnie z zaleceniami SunWuKung, którą powinieneś wybrać): Jedna tabela tłumaczeń dla każdej tabeli "produktu". Oznacza to jeden wiersz na język i kilka pól "tekstowych", więc wymaga tylko jednego (lewego) złączenia na N polach. Następnie możesz łatwo dodać domyślne pole w tabeli "produkt", możesz łatwo usunąć i ponownie wstawić tabelę tłumaczeń i możesz utworzyć drugi tabela dla niestandardowych tłumaczeń (na żądanie), które można również usunąć i wstawić ponownie), a nadal masz wszystkie klucze obce.

Zróbmy przykład, aby zobaczyć to działa:

Najpierw Utwórz tabele:

CREATE TABLE [dbo].[T_Languages](
    [Lang_ID] [int] NOT NULL,
    [Lang_NativeName] [nvarchar](200) NULL,
    [Lang_EnglishName] [nvarchar](200) NULL,
    [Lang_ISO_TwoLetterName] [varchar](10) NULL,
 CONSTRAINT [PK_T_Languages] PRIMARY KEY CLUSTERED 
(
    [Lang_ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO




CREATE TABLE [dbo].[T_Products](
    [PROD_Id] [int] NOT NULL,
    [PROD_InternalName] [nvarchar](255) NULL,
 CONSTRAINT [PK_T_Products] PRIMARY KEY CLUSTERED 
(
    [PROD_Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO





CREATE TABLE [dbo].[T_Products_i18n](
    [PROD_i18n_PROD_Id] [int] NOT NULL,
    [PROD_i18n_Lang_Id] [int] NOT NULL,
    [PROD_i18n_Text] [nvarchar](200) NULL,
 CONSTRAINT [PK_T_Products_i18n] PRIMARY KEY CLUSTERED 
(
    [PROD_i18n_PROD_Id] ASC,
    [PROD_i18n_Lang_Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

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]  WITH CHECK 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]  WITH CHECK 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_Languages]
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] [nvarchar](200) NULL,
 CONSTRAINT [PK_T_Products_i18n_Cust] PRIMARY KEY CLUSTERED 
(
    [PROD_i18n_Cust_PROD_Id] ASC,
    [PROD_i18n_Cust_Lang_Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[T_Products_i18n_Cust]  WITH CHECK ADD  CONSTRAINT [FK_T_Products_i18n_Cust_T_Languages] FOREIGN KEY([PROD_i18n_Cust_Lang_Id])
REFERENCES [dbo].[T_Languages] ([Lang_ID])
GO

ALTER TABLE [dbo].[T_Products_i18n_Cust] CHECK CONSTRAINT [FK_T_Products_i18n_Cust_T_Languages]
GO

--ALTER TABLE [dbo].[T_Products_i18n_Cust]  WITH NOCHECK ADD  CONSTRAINT [FK_T_Products_i18n_Cust_T_Products] FOREIGN KEY([PROD_i18n_Cust_PROD_Id])
ALTER TABLE [dbo].[T_Products_i18n_Cust]  WITH CHECK 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

Następnie wypełnij dane

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

A następnie odpytywać dane:

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

Jeśli jesteś leniwy, możesz również użyć ISO-TwoLetterName ('DE',' EN', itp.) jako klucz podstawowy tabeli języków, nie musisz szukać identyfikatora języka. Ale jeśli tak więc może chcesz użyć IETF-language tag , co jest lepsze, ponieważ dostajesz de-CH i de-DE, co naprawdę nie jest tą samą ortografią (podwójne s zamiast ß wszędzie), chociaż jest to ten sam język bazowy. To tylko mały szczegół, który może być dla ciebie ważny, zwłaszcza biorąc pod uwagę, że en-US i en-GB/en-CA/en-AU lub fr-FR/fr-CA ma podobne problemy.
cytat: nie potrzebujemy go, robimy tylko nasze oprogramowanie w języku angielskim.
odpowiedź: Tak - ale który ??

W każdym razie, jeśli używasz integer ID, jesteś elastyczny i możesz zmienić metodę w dowolnym momencie.
I powinieneś użyć tej liczby całkowitej, ponieważ nie ma nic bardziej irytującego, destrukcyjnego i kłopotliwego niż spartaczona konstrukcja Db.

Zobacz też RFC 5646, ISO 639-2,

I, jeśli nadal mówisz "my" tylko składamy wniosek o "tylko jedną kulturę" (jak zwykle en-US) - dlatego nie potrzebuję tego dodatkowego integer, to byłby dobry czas i miejsce, aby wspomnieć IANA language tags , czyż nie ?
Bo idą tak:

de-DE-1901
de-DE-1996

I

de-CH-1901
de-CH-1996
W 1996 roku przeprowadzono reformę ortografii...) Spróbuj znaleźć słowo w słowniku, jeśli jest źle napisane; staje się to bardzo ważne w aplikacjach zajmujących się portalami prawnymi i publicznymi.
Co ważniejsze, są regiony, które zmieniają się z cyrylicy na alfabet łaciński, który może być po prostu bardziej kłopotliwa niż powierzchowna uciążliwość jakiejś niejasnej reformy ortografii, dlatego też może to być ważna kwestia, w zależności od kraju, w którym mieszkasz. Tak czy inaczej, lepiej mieć tę liczbę całkowitą, na wszelki wypadek...

Edit:
I dodając ON DELETE CASCADE po

REFERENCES [dbo].[T_Products] ([PROD_Id])

Możesz po prostu powiedzieć: DELETE FROM T_Products i nie uzyskać naruszenia klucza obcego.

Co do zestawiania, to zrobiłbym to tak:

A) mieć swój własny DAL
B) Zapisz żądaną nazwę kolacji w tabeli języka

Możesz umieścić zestawienia we własnej tabeli, np.:

SELECT * FROM sys.fn_helpcollations() 
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%' 

C) mieć nazwę sortowania dostępną w auth.użytkownik.informacje o języku

D) napisz swój SQL tak:

SELECT 
    COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
FROM T_Groups 

ORDER BY GroupName COLLATE {#COLLATION}

E) wtedy możesz to zrobić w swoim DAL:

cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)

Który następnie da ci to doskonale skomponowane zapytanie 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
 45
Author: Stefan Steiger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-04 10:30:11

Spójrz na ten przykład:

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)
)

Myślę, że nie ma potrzeby wyjaśniać, struktura sama się opisuje.

 9
Author: bamburik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-12 07:22:08

Zwykle wybrałbym takie podejście (nie rzeczywiste sql), odpowiada to ostatniej opcji.

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'

Ponieważ posiadanie wszystkich możliwych do przetłumaczenia tekstów w jednym miejscu sprawia, że konserwacja jest o wiele łatwiejsza. Czasami tłumaczenia są zlecane biurom tłumaczeń, w ten sposób możesz wysłać im tylko jeden duży plik eksportowy i równie łatwo zaimportować go z powrotem.

 8
Author: user39603,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-25 09:37:47

Przed przejściem do szczegółów technicznych i rozwiązań, należy zatrzymać się na chwilę i zadać kilka pytań na temat wymagań. Odpowiedzi mogą mieć ogromny wpływ na rozwiązanie techniczne. Przykłady takich pytań to:
- Czy wszystkie języki będą używane przez cały czas?
- Kto i kiedy wypełni kolumny różnymi wersjami językowymi?
- Co się dzieje, gdy użytkownik będzie potrzebował określonego języka tekstu i nie ma go w systemie?
- Tylko teksty są być zlokalizowane lub istnieją również inne przedmioty (na przykład cena może być przechowywana w $ I€, ponieważ mogą być różne)

 3
Author: Aleris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-25 09:59:32

Szukałem wskazówek dotyczących lokalizacji i znalazłem ten temat. Zastanawiałem się dlaczego to jest używane:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Więc masz coś takiego jak user39603 sugeruje:

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'

Nie możesz po prostu zostawić tłumaczenia tabeli, więc dostaniesz to:

    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'
 3
Author: randomizer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-06 18:00:39

Zgadzam się z randomizerem. Nie rozumiem, dlaczego potrzebujesz tabeli "tłumaczenie".

Myślę, że to wystarczy:

TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName
 1
Author: Bart VW,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-04 09:31:22

Czy poniższe podejście byłoby realne? Powiedzmy, że masz tabele, w których więcej niż 1 kolumna wymaga tłumaczenia. Tak więc w przypadku produktu możesz mieć zarówno nazwę produktu , jak i opis produktu, które wymagają tłumaczenia. Czy można wykonać następujące czynności:

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)
    )   
 1
Author: davey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-13 16:03:31

"który z nich jest najlepszy" opiera się na sytuacji projektu. Pierwszy z nich jest łatwy do wyboru i utrzymania, a także wydajność jest najlepsza, ponieważ nie trzeba łączyć tabel podczas wybierania jednostki. Jeśli potwierdziłeś, że Twój obiekt obsługuje tylko 2 lub 3 Języki i nie zwiększy się, możesz go użyć.

Drugi jest okey, ale jest trudny do zrozumienia i utrzymania. A wydajność jest gorsza niż pierwsza.

Ostatni jest dobry w skalowalności, ale zły w wydajność. Tabela t_translation_entry staje się coraz większa, to straszne, gdy chcesz pobrać listę encji z niektórych tabel.

 0
Author: studyzy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-03 05:34:02