Схема багатомовної бази даних


235

Я розробляю багатомовне програмне забезпечення. Що стосується коду програми, локалізація не є проблемою. Ми можемо використовувати конкретні мовні ресурси та мати всі види інструментів, які добре працюють з ними.

Але який найкращий підхід у визначенні багатомовної схеми бази даних? Скажімо, у нас є багато таблиць (100 і більше), і кожна таблиця може мати кілька стовпців, які можна локалізувати (більшість стовпців nvarchar повинні бути локалізованими). Наприклад, одна з таблиць може містити інформацію про товар:

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

Я можу придумати три підходи до підтримки багатомовного тексту у стовпцях NAME та DESCRIPTION:

  1. Окремий стовпчик для кожної мови

    Коли ми додаємо нову мову до системи, ми повинні створити додаткові стовпці для зберігання перекладеного тексту, наприклад:

    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. Таблиця перекладу зі стовпцями для кожної мови

    Замість зберігання перекладеного тексту зберігається лише зовнішній ключ до таблиці перекладів. Таблиця перекладів містить стовпчик для кожної мови.

    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. Таблиці перекладу з рядками для кожної мови

    Замість зберігання перекладеного тексту зберігається лише зовнішній ключ до таблиці перекладів. Таблиця перекладів містить лише ключ, а окрема таблиця містить рядки для кожного перекладу на мову.

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

У кожному рішенні є плюси і мінуси, і я хотів би знати, який у вас досвід роботи з цими підходами, що ви рекомендуєте і як би ви вирішили розробити схему багатомовної бази даних.



3
Ви можете перевірити це посилання: gsdesign.ro/blog/multilanguage-database-design-approach, хоча читання коментарів дуже корисне
Fareed Alnamrouti

3
LANGUAGE_CODEє природним ключем, уникайте LANGUAGE_ID.
gavenkoa

1
Я вже бачив / використовував 2 і 3, я не рекомендую їх, ви легко закінчите осиротіли ряди. Дизайн @SunWiKung краще виглядає IMO.
Guillaume86

4
Я віддаю перевагу дизайну SunWuKungs, який збіг випадково - це ми реалізували. Однак вам потрібно врахувати порівняння. Принаймні, у сервері Sql кожен стовпець має властивість зіставлення, яке визначає такі речі, як чутливість до регістру, еквівалентність (чи ні) акцентованих символів та інші міркування, що стосуються мови. Чи ви використовуєте відповідність для мови чи ні, залежить від вашого загального дизайну програми, але якщо ви помилитесь, пізніше це буде важко змінити. Якщо вам потрібні конкретні зіставлення для мови, вам знадобиться стовпець на кожній мові, а не рядок на кожній мові.
Елрой Флінн

Відповіді:


113

Що ви думаєте про наявність відповідної таблиці перекладу для кожної таблиці, що перекладається?

СТВОРИТИ ТАБЛИЦУ T_PRODUCT (pr_id int, ЦІНА НОМЕР (18, 2))

СТВОРИТИ ТАБЛИЦУ T_PRODUCT_tr (pr_id INT FK, мовний код varchar, текст прізвища, текст pr_descr)

Таким чином, якщо у вас є кілька перекладних стовпців, для його отримання буде потрібно лише одне з'єднання, оскільки ви не автогенеруєте транслаціонід, можливо імпортувати елементи разом із відповідними перекладами.

Негативна сторона цього полягає в тому, що якщо у вас є складний механізм резервного мовлення, вам може знадобитися реалізувати це для кожної таблиці перекладу - якщо ви покладаєтесь на якусь збережену процедуру. Якщо це зробити з програми, це, ймовірно, не буде проблемою.

Дайте мені знати, що ви думаєте - я також збираюся прийняти рішення з цього приводу нашої наступної заяви. Поки ми використовували ваш 3-й тип.


2
Цей варіант схожий на мій варіант № 1, але краще. Це все ще важко підтримувати і вимагає створення нових таблиць для нових мов, тому я б не хотів його реалізовувати.
qbeuek

28
для цього не потрібна нова таблиця для нової мови - ви просто додаєте новий рядок у відповідну таблицю _tr за допомогою нової мови, вам потрібно створити нову таблицю _tr, лише якщо ви створите нову таблицю, що перекладається

3
я вважаю, що це хороший метод. інші методи вимагають тонн лівих приєднань, і коли ви приєднуєтесь до декількох таблиць, що кожна з них має переклад, як 3 рівня глибиною, і в кожному є 3 поля, вам потрібно 3 * 3 9 зліва приєднатися лише для перекладів .. інші мудрі 3. Також це легше додавати обмеження тощо, і я вважаю, що пошук є більш резонансним.
GorillaApe

1
Коли на T_PRODUCT1 мільйон рядків T_PRODUCT_trбуде 2 мільйони. Чи може це значно знизити ефективність sql?
Міфріл

1
@Mithril У будь-якому випадку у вас є 2 мільйони рядків. Принаймні, вам не потрібно приєднуватися до цього методу.
Девід Д

56

Це цікаве питання, тож давайте некромантизувати.

Почнемо з проблем методу 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>схема змінюється через те, що раптом змінюються вимоги до крила, і створюється нова таблиця, стара залишається, але посилання перейменовано, а ви не отримуєте жодного попередження. Звіт працює навіть при його запуску, не вибираючи параметр крила (==> guide.empty). Але раптом, коли фактично користувач фактично вибирає крило ==>бум . Цей метод повністю порушує будь-який вид тестування.


Спосіб 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 () [або шляхом введення ідентифікатора у вкладиші, але мають ідентифікатор-вставку OFF ], і це (і буде) призведе до сміття даних (і виключення з нульовим посиланням) дуже скоро.


Спосіб 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&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 

Тоді ви можете отримати значення за 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 "&quot;I am a ''value &quot;"')
WHERE id = 1 

Де ви можете замінити /lang/de/...з'.../' + @in_language + '/...'

Наче подібний hstore PostGre, за винятком того, що через накладні розбори XML (замість того, щоб прочитати запис з асоціативного масиву в PG hstore), він стає занадто повільним, плюс кодування xml робить його занадто болісним, щоб бути корисним.


Спосіб 5 (за рекомендацією SunWuKung, той, який ви повинні вибрати): одна таблиця перекладу для кожної таблиці "Продукт". Це означає один рядок на кожній мові та кілька полів "текст", тому для N полів потрібно приєднати лише ОДНЕ (ліве). Тоді ви можете легко додати поле за замовчуванням у таблицю "Продукт", ви можете легко видалити та повторно вставити таблицю перекладу, а також можете створити другу таблицю для користувацьких перекладів (за запитом), яку ви також можете видалити і знову вставити), і у вас ще є всі іноземні ключі.

Зробимо приклад, щоб побачити цю РОБОТУ:

Спочатку створіть таблиці:

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 , що краще, тому що ви отримуєте 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і не мати жодного порушення зовнішнього ключа.

Що стосується порівняння, я б це робив так:

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-запит

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

ORDER BY GroupName COLLATE German_PhoneBook_CI_AI

Хороша детальна відповідь, велике спасибі. Але що ви думаєте про проблеми зіставлення у вирішенні методу 5. Здається, це не найкращий спосіб, коли вам потрібно було сортувати або фільтрувати перекладений текст у багатомовній середовищі з різними зіставленнями. І в такому випадку спосіб 2 (який ви так швидко "відключили" :)) може бути кращим варіантом з незначними модифікаціями, що вказують на співвідношення цілей для кожного локалізованого стовпця.
Євген Євдокимов

2
@ Євген Євдокімов: Так, але "ЗАМОВИТИ БУК" завжди буде проблемою, тому що ви не можете вказати це як змінну. Мій підхід полягав би в тому, щоб зберегти ім'я зіставлення в мовній таблиці та мати це у користувацькій інформації. Потім у кожному SQL-Заяві ви можете сказати ЗАМОВЛЕННЯ COLUMN_NAME {#collation}, а потім можете зробити заміну у своєму dal (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user Ви також можете сортувати свій код програми, наприклад, використовуючи LINQ. Це також займе деяке завантаження вашої бази даних. Для звітів все одно впорядковується
Стефан Штайгер

oo Це повинна бути найдовша відповідь, яку я бачив, і я бачив, як люди складають цілі програми у відповідях. Ти хороший.
Доміно

Цілком можна погодитися, рішення SunWuKung найкраще
Домі

48

Третій варіант найкращий з кількох причин:

  • Не вимагає зміни схеми бази даних для нових мов (і, таким чином, обмеження змін коду)
  • Не потрібно багато місця для бездоганних мов або перекладів певного предмета
  • Забезпечує найбільшу гнучкість
  • Ви не закінчите з розрідженими таблицями
  • Вам не потрібно турбуватися про нульові ключі та перевіряти, чи відображається наявний переклад замість якогось нульового запису.
  • Якщо ви змінюєте або розширюєте свою базу даних, щоб охоплювати інші перекладаються елементи / речі / тощо, ви можете використовувати ті самі таблиці та систему - це дуже від'єднано від решти даних.

-Адам


1
Я погоджуюся, хоча особисто я мав би локалізовану таблицю для кожної основної таблиці, щоб дозволити реалізацію сторонніх ключів.
Ніл Барнвелл

1
Хоча третій варіант є найбільш чистою та надійною реалізацією проблеми, вона є більш складною, ніж перша. Я думаю, що для демонстрації, редагування, надання звітів про загальну версію потрібно так багато додаткових зусиль, що не завжди є прийнятним. Я реалізував обидва рішення, простішого було достатньо, коли користувачі потребували перекладу "основної" мови додатків лише для читання (іноді відсутнього).
rics

12
Що робити, якщо таблиця продукту містить кілька перекладених полів? Отримавши продукти, вам доведеться зробити ще одне додаткове приєднання за перекладене поле, що призведе до серйозних проблем з продуктивністю. Існує також додаткова складність для вставки / оновлення / видалення. Єдиною перевагою цього є менша кількість таблиць. Я б зайнявся методом, запропонованим SunWuKung: Я думаю, що це хороший баланс між питаннями продуктивності, складності та обслуговування.
Морозний Z

@ rics- Я згоден, ну що ви пропонуєте ...?
шабля

@ Адам- Я розгублений, можливо, я зрозумів неправильно. Ви запропонували третій, правда? Поясніть, будь ласка, більш докладно, яким чином будуть відносини між цими таблицями? Ви маєте на увазі, що ми повинні реалізувати таблиці Translate and TranslationEntry для кожної таблиці в БД?
шабля

9

Погляньте на цей приклад:

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

Я думаю, що пояснювати не потрібно, структура описує себе.


це добре. але як би ви шукали (наприклад, ім'я продукту)?
Ілюмінати

Чи був у вас живий приклад десь із вашої вибірки? Чи виникли якісь проблеми, використовуючи його?
Девід Летурно

Звичайно, у мене є багатомовний проект нерухомості, ми підтримуємо 4 мови. Пошук трохи складний, але його швидкий. Звичайно, у великих проектах це може бути повільніше, ніж потрібно. У малих та середніх проектах це нормально.
бамбук

8

Зазвичай я б пішов на такий підхід (не фактичний sql), це відповідає вашому останньому варіанту.

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'

Оскільки наявність усіх текстів, що можна перекладати в одному місці, суттєво полегшує обслуговування. Іноді переклади передаються в бюро перекладів, таким чином ви можете надіслати їм лише один великий експортний файл і так само легко імпортувати його назад.


1
Якій цілі служить Translationтаблиця або TranslationItem.translationitemidстовпець?
DanMan

4

Перш ніж перейти до технічних деталей та рішень, слід зупинитися на хвилину і задати кілька запитань щодо вимог. Відповіді можуть мати величезний вплив на технічне рішення. Прикладами таких питань є:
- Чи будуть усі мови використовуватися постійно?
- Хто і коли заповнить стовпці різними мовними версіями?
- Що станеться, коли користувачеві знадобиться певна мова тексту, а його немає в системі?
- Локалізуються лише тексти або також є інші елементи (наприклад, PRICE можна зберігати в $ і €, оскільки вони можуть бути різними)


Я знаю, що локалізація - це набагато ширша тема, і я знаю про проблеми, які ви мені донесли, але в даний час я шукаю відповідь на дуже конкретну проблему дизайну схем. Я припускаю, що нові мови будуть додаватися поступово, і кожна буде перекладена майже повністю.
qbeuek

3

Я шукав поради щодо локалізації та знайшов цю тему. Мені було цікаво, чому це використовується:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

Отже, ви отримуєте щось на кшталт user39603:

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'

Не можете ви просто залишити таблицю Переклад, щоб ви отримали це:

    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'

1
Звичайно. Я б назвав ProductItemстіл чимось подібним ProductTextsчи ProductL10nхоч. Має більше сенсу.
DanMan

1

Я погоджуюсь з рандомізатором. Я не бачу, навіщо вам потрібна таблиця "переклад".

Я думаю, цього достатньо:

TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName

1

Чи був би нижченаведений підхід життєздатним? Скажімо, у вас є таблиці, де потрібно перекласти більше 1 стовпця. Отже, для продукту у вас може бути назва продукту та опис продукту, які потребують перекладу. Чи можете ви зробити наступне:

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

0

"Хто найкращий" ґрунтується на проектній ситуації. Перший легко вибрати і підтримувати, а також найкраща продуктивність, оскільки йому не потрібно приєднуватися до таблиць при виборі об'єкта. Якщо ви підтвердили, що ваш проект підтримує лише 2 або 3 мови, і він не збільшиться, ви можете використовувати його.

Другий - це добре, але важко зрозуміти та підтримувати. А продуктивність гірша, ніж перша.

Останній - хороший у масштабованості, але поганий у роботі. Таблиця T_TRANSLATION_ENTRY стане все більшою і більшою, це жахливо, коли ви хочете отримати список об’єктів з деяких таблиць.


0

У цьому документі описані можливі рішення та переваги та недоліки кожного методу. Я віддаю перевагу "локалізації рядків", оскільки вам не потрібно змінювати схему БД при додаванні нової мови.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.