Схема для мультиязычной базы данных


236

Я занимаюсь разработкой мультиязычного программного обеспечения. Что касается кода приложения, локализуемость не является проблемой. Мы можем использовать языковые ресурсы и иметь все виды инструментов, которые хорошо с ними работают.

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

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

Я могу подумать о трех подходах к поддержке многоязычного текста в столбцах ИМЯ и ОПИСАНИЕ:

  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 Server каждый столбец имеет свойство сортировки, которое определяет такие вещи, как чувствительность к регистру, эквивалентность (или нет) акцентированных символов и другие специфические для языка соображения. Используете ли вы языковые сопоставления или нет, зависит от вашего общего дизайна приложения, но если вы ошибетесь, это будет сложно изменить позже. Если вам нужны языковые параметры сортировки, вам потребуется столбец для каждого языка, а не строка для каждого языка.
Элрой Флинн

Ответы:


114

Что вы думаете о наличии связанной таблицы перевода для каждой переводимой таблицы?

CREATE TABLE T_PRODUCT (pr_id int, PRICE NUMBER (18, 2))

CREATE TABLE T_PRODUCT_tr (pr_id INT FK, код языка varchar, текст pr_name, текст pr_descr)

Таким образом, если у вас есть несколько переводимых столбцов, потребуется только одно объединение, чтобы получить его +, поскольку вы не генерируете автоматический перевод, может быть проще импортировать элементы вместе с соответствующими переводами.

Отрицательной стороной этого является то, что если у вас есть сложный механизм восстановления языка, вам может потребоваться реализовать его для каждой таблицы перевода - если вы полагаетесь на какую-то хранимую процедуру для этого. Если вы сделаете это из приложения, это, вероятно, не будет проблемой.

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


2
Этот вариант похож на мой вариант № 1, но лучше. Это все еще сложно поддерживать и требует создания новых таблиц для новых языков, поэтому я бы неохотно его реализовал.
Qbeuek

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

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

1
Когда T_PRODUCTбудет 1 миллион строк, 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>схема изменится из-за внезапного изменения требований к крылу и создания новой таблицы, старая останется, но поле ссылки будет переименовано, вы не получите никакого предупреждения. Отчет даже работает, когда вы запускаете его без выбора параметра wing (==> guid.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 + '/...'

В некотором роде, как в PostGre hstore, за исключением того, что из-за накладных расходов на синтаксический анализ XML (вместо чтения записи из ассоциативного массива в PG hstore) он становится слишком медленным, а кодирование xml делает его слишком болезненным, чтобы быть полезным.


Метод 5 (в соответствии с рекомендацией SunWuKung, который вы должны выбрать): одна таблица перевода для каждой таблицы «Продукт». Это означает одну строку на язык и несколько «текстовых» полей, поэтому требуется только ОДНО (слева) соединение по N полям. Затем вы можете легко добавить поле по умолчанию в «Product» -таблицу, вы можете легко удалить и повторно вставить таблицу перевода, и вы можете создать вторую таблицу для пользовательских переводов (по запросу), которую вы также можете удалить и вставьте заново), и у вас все еще есть все внешние ключи.

Давайте сделаем пример, чтобы увидеть это работает:

Сначала создайте таблицы:

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-language , что лучше, потому что вы получаете de-CH и de-DE, что на самом деле не одинаково для орфографии (double 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
@ Евгений Евдокимов: Да, но «ORDER BY» всегда будет проблемой, потому что вы не можете указать его как переменную. Мой подход заключается в том, чтобы сохранить имя сопоставления в языковой таблице и сохранить его в userinfo. Затем в каждом выражении SQL вы можете сказать ORDER BY COLUMN_NAME {#collation}, а затем вы можете выполнить замену в своем dal (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user). language.collation.) Кроме того, вы можете отсортировать код своего приложения, например, с помощью LINQ. Это также потребует некоторой обработки от вашей базы данных. Для отчетов отчет все равно сортируется.
Stefan Steiger

oo Это, наверное, самый длинный SO-ответ, который я видел, и я видел, как люди делают целые программы в ответах. Ты в порядке.
Домино

Могу полностью согласиться с тем, что решение SunWuKung является лучшим
Domi

48

Третий вариант является лучшим по нескольким причинам:

  • Не требует изменения схемы базы данных для новых языков (и, следовательно, ограничения изменений кода)
  • Не требует много места для нереализованных языков или переводов определенного предмета
  • Обеспечивает максимальную гибкость
  • Вы не заканчиваете с разреженными таблицами
  • Вам не нужно беспокоиться о нулевых ключах и проверке того, что вы отображаете существующий перевод вместо какой-либо нулевой записи.
  • Если вы измените или расширите свою базу данных, включив в нее другие переводимые элементы / вещи / и т. Д., Вы можете использовать те же таблицы и системы - это очень не связано с остальными данными.

-Адам


1
Я согласен, хотя лично у меня будет локализованная таблица для каждой основной таблицы, чтобы можно было реализовать внешние ключи.
Нил Барнвелл

1
Хотя третий вариант является наиболее чистой и надежной реализацией проблемы, он является более сложным, чем первый. Я думаю, что отображение, редактирование, представление общей версии требует так много дополнительных усилий, что это не всегда приемлемо. Я реализовал оба решения, более простого было достаточно, когда пользователям нужен был только для чтения (иногда отсутствует) перевод «основного» языка приложения.
ric

12
Что если таблица продуктов содержит несколько переведенных полей? При получении продуктов вам придется выполнить одно дополнительное объединение для каждого переведенного поля, что приведет к серьезным проблемам с производительностью. Также (IMO) есть дополнительная сложность для вставки / обновления / удаления. Единственным преимуществом этого является меньшее количество таблиц. Я бы остановился на методе, предложенном SunWuKung: я думаю, что это хороший баланс между производительностью, сложностью и проблемами обслуживания.
Морозный Z

@ rics- Я согласен, что ты предлагаешь ...?
сабля

@ Адам, я в замешательстве, может, меня неправильно поняли. Вы предложили третий, верно? Пожалуйста, объясните это более подробно, как будут складываться отношения между этими таблицами? Вы имеете в виду, что мы должны реализовать таблицы Translation и 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)
)

Я думаю, что нет необходимости объяснять, структура описывает себя.


это хорошо. но как вы будете искать (например, product_name)?
Иллюминаты

У вас был живой пример где-то из вашего образца? У вас возникли проблемы с его использованием?
Дэвид Летурно

Конечно, у меня многоязычный проект по недвижимости, мы поддерживаем 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.