Рекомендации по локализации базы данных SQL Server (2005/2008)

Вопрос

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

Например, классическая таблица "Категория" может иметь столбец "Имя" и "Описание", который должен быть глобализирован. Один из способов - создать таблицу "Текст" для каждого из ваших сущностей, а затем выполнить соединение, чтобы получить значения на основе предоставленного языка.

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

Мне любопытно, есть ли какие-либо, задокументированные, лучшие практики/шаблоны проектирования при внедрении такой поддержки в базу данных SQL Server 2005/2008 (я имею в виду RDBMS, поскольку он может содержать поддерживаемые ключевые слова и что помогает при реализации)?

Мысли о подходе XML

Одной из идей, с которой я работал (хотя и до сих пор в моей голове), было использование типа данных XML, представленного в SQL Server 2005. Идея заключалась в том, чтобы создавать столбцы, которые должны поддерживать локализацию, типа данных XML (и связывать схема к нему). XML будет содержать локализованные строки вместе с кодом языка/культурой, к которому он привязан.

Что-то вдоль линий

Product
ID (int, identity)
Name (XML ...)
Description (XML ...)

Тогда у вас будет что-то вроде этого как XML

<localization>
  <text culture="sv-SE">Detta är ett namn</text>
  <text culture="en-EN">This is a name</text>
</localization>

Тогда вы можете сделать (это не производственный код, поэтому я буду использовать *)

SELECT *
From Product
Where Product.ID = 10

И вы вернете продукт со всеми локализованными текстами, что означало бы, что вам придется делать извлечение на стороне клиента. Самой большой проблемой здесь является, очевидно, количество дополнительных данных, которые вам придется возвращать по каждому запросу. Преимущества будут более чистым, без таблиц поиска, объединений и т.д.

Btw, какой бы метод я ни использовал в своем проекте, я все равно буду использовать Linq To SQL (.NET Platform) для запроса базы данных (подход XML должен быть проблемой, поскольку он вернет XElement, который может быть интерпретированная клиентская сторона)

Поэтому предложение о шаблонах проектирования локализации базы данных и, возможно, комментарии к мысли XML, было бы очень оптимизировано.

Ответы

Ответ 1

Я думаю, вы можете придерживаться XML, который позволяет использовать более чистый дизайн. Я бы пошел дальше и воспользовался атрибутом xml:lang, который предназначен для этого использования:

<l10n>
  <text xml:lang="sv-SE">Detta är ett namn</text>
  <text xml:lang="en-EN">This is a name</text>
</l10n>

Еще один шаг, вы можете выбрать локализованный ресурс в своем запросе через запрос XPath (как это предложено в комментариях), чтобы избежать лечение на стороне клиента. Это даст что-то вроде этого (untested):

SELECT Name.value('(l10n/text[lang()="en"])[1]', 'NVARCHAR(MAX)')
  FROM Product
  WHERE Product.ID=10;

Обратите внимание, что это решение будет элегантным, но менее эффективным решением, чем отдельная таблица. Что может быть в порядке для некоторых приложений.

Ответ 2

Вот как я это сделал. Я не использую LINQ или SP для этого, потому что запрос слишком сложный и динамически построен, и это всего лишь отрывок запроса.

У меня есть таблица продуктов:

* id
* price
* stocklevel
* active
* name
* shortdescription
* longdescription

и таблица товаров: globalization:

* id
* products_id
* name
* shortdescription
* longdescription

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

Я предпочитаю иметь таблицу "бок о бок" над глобальным ресурсом, потому что в некоторых ситуациях вам может понадобиться сделать, например, базу данных (MySQL) MATCH на пару столбцов, таких как MATCH (имя, короткое описание, длинный список) ПРОТИВ ( "Что-то здесь" ).

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

Псевдо:

string query = "";
if(string.IsNullOrEmpty(culture)) {
   // No culture specified, no join needed.
   query = "SELECT p.price, p.name, p.shortdescription FROM products p WHERE p.price > ?Price";
} else {
   query = "SELECT p.price, case when pg.name is null then p.name else pg.name end as 'name', case when pg.shortdescription is null then p.shortdescription else pg.shortdescription end as 'shortdescription' FROM products p"
   + " LEFT JOIN products_globalization pg ON pg.products_id = p.id AND pg.culture = ?Culture"
   + " WHERE p.price > ?Price";
}

Я бы пошел с COALESCE вместо CASE ELSE, но это не так.

Что ж, это мое занятие. Не стесняйтесь критиковать мое предложение...

С уважением, Ричард

Ответ 3

Я не понимаю, почему вам нужно несколько текстовых таблиц. Должна быть достаточной единая текстовая таблица с уникальным идентификатором текста в глобальном масштабе. В таблице будут ID, язык, текстовые столбцы, и вы получите только текст на том языке, который вам нужно представить (или, возможно, вообще не получить текст). Соединение должно быть достаточно эффективным, поскольку комбинация (ID, язык) является первичным ключом.

Ответ 4

Это один из вопросов, которые трудно ответить, потому что в ответе так много "зависит": -)

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

В общих терминах я обычно использую один из двух подходов:

  • Сохраните локализованные элементы рядом с исполняемым файлом (локализованные DLL файлы)
  • Храните локализованные элементы в БД и введите столбец localeID в таблицах, содержащих локализованные элементы.

Преимущество первого метода - хорошая поддержка VisualStudio. Преимуществом второго является централизованное развертывание.

Ответ 5

Я не вижу преимущества в использовании XML-столбцов для хранения локализованных значений. Кроме того, возможно, что у вас есть все локализованные версии одного элемента "в одном месте", если это вам что-то стоит.

Я бы предложил использовать столбец cultureID в каждой таблице с локализуемыми элементами. Таким образом, вам вообще не нужна обработка XML. У вас уже есть ваши данные в реляционной схеме, поэтому зачем вводить другой уровень сложности, когда реляционная схема отлично справляется с этой проблемой?

Скажем, что "sv-SE" имеет культуруID = 1, а "en-EN" имеет 2.

Затем ваш запрос будет изменен как

SELECT *
From Product
Where Product.ID = 10 AND Product.cultureID = 1

для шведского клиента.

Это решение я часто видел в локализованных базах данных. Он хорошо масштабируется как с количеством культур, так и с количеством наборов данных. Это позволяет избежать XML-анализа и обработки и легко реализовать.

И еще один момент: XML-решение дает вам гибкость, которая вам не нужна: вы можете, например, взять "sv-SE" -значение из "Name" -column и "en-EN" -значение из столбца "Описание". Однако вам это не нужно, поскольку ваш клиент будет запрашивать только одну культуру за раз. Гибкость обычно имеет стоимость. В этом случае вам нужно разобрать все столбцы индивидуально, в то время как с помощью решения cultureID вы получите всю запись со всеми значениями, подходящими для запрашиваемой культуры.

Ответ 6

Мне нравится подход XML, потому что отдельное-табличное решение НЕ вернет результат, если, например, нет шведского перевода (cultureID = 1), если вы не выполняете внешнее соединение. Но, тем не менее, вы не можете вернуться на английский язык. С помощью подхода XML вы просто можете вернуться на английский язык. Любые новости по XML-подходу в среде producitve?

Ответ 7

Вот некоторые thoghts в блоге Rick Strahl:

Локализация базы данных Локализация JavaScript

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

CREATE TABLE [dbo].[Lang_en_US_Msg](
    [MsgId] [int] IDENTITY(1,1) NOT NULL,
    [MsgKey] [varchar](200) NOT NULL,
    [MsgTxt] [varchar](2000) NOT NULL,
    [MsgDescription] [varchar](2000) NOT NULL,
 CONSTRAINT [PK_Lang_US-us__Msg] PRIMARY KEY CLUSTERED 
(
    [MsgId] 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].[User](
    [UserId] [int] IDENTITY(1,1) NOT NULL,
    [FirstName] [varchar](50) NOT NULL,
    [MiddleName] [varchar](50) NULL,
    [LastName] [varchar](50) NULL,
    [DomainName] [varchar](50) NULL,
 CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
(
    [UserId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE TABLE [dbo].[UserSetting](
    [UserSettingId] [int] IDENTITY(1,1) NOT NULL,
    [UserId] [int] NOT NULL,
    [CultureInfo] [varchar](50) NOT NULL,
    [GuiLanguage] [varchar](10) NOT NULL,
 CONSTRAINT [PK_UserSetting] PRIMARY KEY CLUSTERED 
(
    [UserSettingId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

ГО

 ALTER TABLE [dbo].[UserSetting] ADD  CONSTRAINT [DF_UserSetting_CultureInfo]  DEFAULT ('fi-FI') FOR [CultureInfo]
 GO

 CREATE TABLE [dbo].[Lang_fi_FI_Msg](
    [MsgId] [int] IDENTITY(1,1) NOT NULL,
    [MsgKey] [varchar](200) NOT NULL,
    [MsgTxt] [varchar](2000) NOT NULL,
    [MsgDescription] [varchar](2000) NOT NULL,
    [DbSysNameForExpansion] [varchar](50) NULL,
 CONSTRAINT [PK_Lang_Fi-fi__Msg] PRIMARY KEY CLUSTERED 
(
    [MsgId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE PROCEDURE [dbo].[procGui_GetPageMsgs]
@domainUser varchar(50) ,           -- the domain_user performing the action  
@msgOut varchar(4000) OUT,        -- the (error) msg to be shown to the user   
@debugMsgOut varchar(4000) OUT ,   -- this variable holds the debug msg to be shown if debug level is enabled   
@ret int OUT                  -- the variable indicating success or failure 

AS                            
BEGIN -- proc start                            
 SET NOCOUNT ON;                            

declare @procedureName varchar(200)        
declare @procStep varchar(4000)  


set @procedureName = ( SELECT OBJECT_NAME(@@PROCID))        
set @msgOut = ' '     
set @debugMsgOut = ' '     
set @procStep = ' '     


BEGIN TRY        --begin try                  
set @ret = 1 --assume false from the beginning                  

--===============================================================
 --debug   set @[email protected] + 'GETTING THE GUI LANGUAGE FOR THIS USER '
--===============================================================

declare @guiLanguage nvarchar(10)




if ( @domainUser is null)
    set @guiLanguage = (select Val from AppSetting where Name='guiLanguage')
else 
    set @guiLanguage = (select GuiLanguage from UserSetting us join [User] u on u.UserId = us.UserId where [email protected])

set @guiLanguage = REPLACE ( @guiLanguage , '-' , '_' ) ;


--===============================================================
set @[email protected] + ' BUILDING THE SQL QUERY '
--===============================================================

DECLARE @sqlQuery AS nvarchar(2000)
SET @sqlQuery = 'SELECT  MsgKey , MsgTxt FROM dbo.lang_' + @guiLanguage + '_Msg'


--===============================================================
set @[email protected] + 'EXECUTING THE SQL QUERY'
--===============================================================
print @sqlQuery

    exec sp_executesql @sqlQuery

    set @debugMsgOut = @procStep
    set @ret = @@ERROR                  


END TRY        --end try                  

BEGIN CATCH                        
 PRINT 'In CATCH block.                         
 Error number: ' + CAST(ERROR_NUMBER() AS varchar(10)) + '                        
 Error message: ' + ERROR_MESSAGE() + '                        
 Error severity: ' + CAST(ERROR_SEVERITY() AS varchar(10)) + '                        
 Error state: ' + CAST(ERROR_STATE() AS varchar(10)) + '                        
 XACT_STATE: ' + CAST(XACT_STATE() AS varchar(10));                        

set @msgOut = 'Failed to execute ' + @sqlQuery             
set @debugMsgOut = ' Error number: ' + CAST(ERROR_NUMBER() AS varchar(10)) +               
 'Error message: ' + ERROR_MESSAGE() + 'Error severity: ' + CAST(ERROR_SEVERITY() AS varchar(10)) +               
 'Error state: ' + CAST(ERROR_STATE() AS varchar(10)) + 'XACT_STATE: ' + CAST(XACT_STATE() AS varchar(10))                        

--record the error in the database                        
--debug    
 --EXEC [dbo].[procUtils_DebugDb]
    --  @DomainUser = @domainUser,
    --  @debugmsg = @debugMsgOut,
    --  @ret = 1,
    --  @procedureName = @procedureName ,
    --  @procedureStep = @procStep

 -- set @ret = 1                       

END CATCH                        


return  @ret                                   
END --procedure end                             

Ответ 8

Я вижу delima в целом - у вас есть единая сущность, которую вы должны представлять как один экземпляр (например, один идентификатор продукта "10" ), но имеет несколько локализованных текстов разных столбцов/свойств. Это очень сложно, и я вижу необходимость в POS-системах, что вы хотите отслеживать только один ProductID = 10, а не несколько продуктов с разными идентификаторами ProductID, но они одинаковы с другим текстом.

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

Основным недостатком является количество данных, передаваемых по кабелю из БД на сервисный уровень /UI/App. Я бы попытался сделать некоторое преобразование на конце SQL, прежде чем возвращать результат, чтобы вернуть только один пользовательский интерфейс для культуры. Вы всегда можете просто выбрать правильную формулу через xml в sproc и вернуть ее как обычный текст.

В целом, это отличается от того, например, сообщение в блоге или необходимость использования CMS для локализации - что я сделал несколько.

Мой подход к Post scenerio будет похож на TToni, за исключением моделирования данных с точки зрения Domain (и прикосновения BDD). С учетом сказанного сосредоточьтесь на том, чего вы хотите достичь:

Given a users culture is "sv-se"
When the user views a post list
It should list posts only in "sv-se" culture

Это означает, что пользователь должен видеть список сообщений только для своей культуры. Способ, которым мы реализовали это раньше, состоял в том, чтобы передать набор культур для запроса на основе того, что пользователь мог видеть. Если пользователь установил "sv-se" в качестве основного, но также выбрал, они говорят на английском языке (en-us), тогда запрос будет выглядеть следующим образом:

SELECT * FROM Post WHERE CultureUI IN ('sv-se', 'en-us')

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

Но чтобы вернуться к первой части моего ответа, ваша потребность проистекает из требования о необходимости использования одного экземпляра с несколькими текстами. Столбец Xml подходит для этого.

Ответ 9

Другой подход к рассмотрению: не хранить контент в базе данных, а поддерживать "приложение", поддерживая записи базы данных и "контент" как отдельные объекты.

Я использовал подобный подход, создавая несколько тем для моего сайта электронной коммерции. Некоторые из продуктов имеют логотип производителя, который также должен соответствовать теме веб-сайта. Поскольку нет реальной поддержки базы данных для тем, у меня была проблема. Решение, с которым я столкнулся, заключалось в том, чтобы использовать токен в базе данных для идентификации идентификатора ClientID изображения, а не для хранения URL-адреса изображения (который зависит от темы).

Следуя тому же подходу, вы можете изменить свою базу данных на хранение имени и описания продукта в хранилище маркера имени и маркера описания, который идентифицировал бы ресурс (в файле resx или в базе данных с использованием подхода Rick Strahl) который содержит контент. Встроенная функциональность .NET тогда будет обрабатывать переключение языка, а не пытаться сделать это в базе данных (редко бывает хорошей идеей поместить бизнес-логику в базу данных). Затем вы можете использовать токен на клиенте для поиска правильного ресурса.

Label1.Text = GetLocalResourceObject("TokenStoredInDatabase").ToString()

Недостаток этого подхода, очевидно, заключается в том, что токены базы данных и токены ресурсов синхронизируются (поскольку продукты могут быть добавлены без каких-либо описаний), но их можно было бы легче сделать с помощью ресурса-ресурса, такого как созданный Rick Strahl. Этот подход может не работать, если у вас есть продукты, которые часто меняются, но для некоторых людей это может быть.

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

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

К счастью, есть простой подход, чтобы обойти это: поместите ссылки на страницу, чтобы перевести ее на другие языки на основе параметра querystring. Пример этого можно найти (oops, они не позволят мне опубликовать другую ссылку!), И если вы отметите, каждая культура страницы была проиндексирована как Google, так и Yahoo (хотя и не Bing). Более продвинутый подход может использовать переписывание URL-адресов в сочетании с некоторыми причудливыми регулярными выражениями, чтобы сделать вашу единую локализованную страницу похожей на несколько каталогов, но на самом деле передать параметр запроса на страницу.

Ответ 10

Индексация становится проблемой. Я не думаю, что вы можете индексировать xml, и, конечно, вы не можете индексировать его, если вы сохраните его как строку, потому что каждая строка начинается с <localization> <text culture="...">.