significa - Mejores prácticas para localizar una base de datos de SQL Server(2005/2008)
restaurar base de datos sql server 2012 a 2008 (10)
Me gusta el enfoque XML, porque la solución de tabla separada NO arrojaría un resultado si, por ejemplo, no hay traducción sueca (cultureID = 1) a menos que haga una combinación externa. Pero, sin embargo, NO puedes volver al inglés. Con el enfoque XML, simplemente puede volver al inglés. ¿Alguna noticia sobre el enfoque XML en un entorno productivo?
Pregunta
Estoy seguro de que muchos de ustedes se han enfrentado al desafío de localizar un back-end de base de datos para una aplicación. Si no lo has hecho, tendré mucha confianza en que las probabilidades de que tengas que hacerlo en el futuro son bastante grandes. Estoy hablando de almacenar múltiples traducciones de textos (y lo mismo puede decirse de la moneda, etc.) para sus entidades de base de datos.
Por ejemplo, la tabla clásica de "Categoría" podría tener una columna Nombre y una Descripción que deberían estar globalizadas. Una forma sería tener una tabla de "Texto" para cada una de sus entidades y luego hacer una combinación para recuperar los valores en función del idioma proporcionado.
Esto te deja con muchas tablas de "Texto", una para cada entidad que deseas localizar, con la adición de un Tipo de Texto para distinguir entre los diversos textos que puede almacenar.
Tengo curiosidad por saber si existen patrones documentados de mejores prácticas / diseño para implementar este tipo de soporte en una base de datos de SQL Server 2005/2008 (estoy siendo específico con respecto al RDBMS, ya que podría contener palabras clave compatibles y eso ayuda con la implementación)?
Pensamientos sobre el enfoque XML
Una idea con la que he estado jugando (aunque solo en mi cabeza hasta ahora) fue aprovechar el tipo de datos XML introducido en SQL Server 2005. La idea era crear columnas que admitieran la localización, del tipo de datos XML (y vincular un esquema al mismo) ) El XML contendría las cadenas localizadas junto con el código de idioma / cultura al que estaba vinculado.
Algo a lo largo de las líneas de
Product
ID (int, identity)
Name (XML ...)
Description (XML ...)
Entonces tendrías algo así como el XML
<localization>
<text culture="sv-SE">Detta är ett namn</text>
<text culture="en-EN">This is a name</text>
</localization>
Entonces podría hacer (Esto no es código de producción, entonces usaré el *)
SELECT *
From Product
Where Product.ID = 10
Y obtendría el producto con todos los textos traducidos, lo que significaría que tendría que realizar la extracción en el lado del cliente. El mayor problema aquí es obviamente la cantidad de datos adicionales que tendría que devolver en cada consulta. Los beneficios serían un diseño más limpio sin tablas de búsqueda, combinaciones, etc.
Por cierto, cualquiera que sea el método que termine utilizando en mi diseño, seguiré utilizando Linq to SQL (plataforma .NET) para consultar la base de datos (el enfoque XML debería ser un problema, ya que devolvería un elemento XElement que podría interpretarse como cliente). lado)
Así que la sugerencia sobre los patrones de diseño de localización de bases de datos, y posiblemente los comentarios sobre el pensamiento XML, sería muy apreciada.
Esa es una de las preguntas que son difíciles de responder porque hay tantas "depende" en la respuesta :-)
La respuesta depende de la cantidad de elementos localizados en la base de datos, en escenarios de implementación, problemas de almacenamiento en caché, patrones de acceso, etc. Si nos puede dar algunos datos sobre cuán grande es la aplicación, cuántos usuarios concurrentes tendrá y cómo se desplegará, sería muy útil.
En términos generales, suelo usar uno de dos enfoques:
- Almacene los elementos localizados cerca del ejecutable (ressource dlls localizadas)
- Almacene elementos localizados en el DB e introduzca una columna localeID en tablas que contienen los elementos localizados.
La ventaja del primer método es la buena compatibilidad con VisualStudio. La ventaja del segundo es el despliegue centralizado.
No veo por qué necesita varias tablas de texto. Una sola tabla de texto, con una ID de texto única "global", debería ser suficiente. La tabla tendría ID, idioma, columnas de texto, y solo obtendría el texto en el idioma que necesita presentar (o quizás no recupere el texto). La unión debe ser bastante eficiente, ya que la combinación de (ID, idioma) es la clave principal.
Creo que puedes seguir con XML, que permite un diseño más limpio. Me gustaría ir más lejos y aprovechar el atributo xml:lang
que está diseñado para este uso :
<l10n>
<text xml:lang="sv-SE">Detta är ett namn</text>
<text xml:lang="en-EN">This is a name</text>
</l10n>
Un paso más, puede seleccionar el recurso localizado en su consulta a través de una consulta XPath (como se sugiere en los comentarios) para evitar cualquier tratamiento del lado del cliente. Esto daría algo como esto (no probado):
SELECT Name.value(''(l10n/text[lang()="en"])[1]'', ''NVARCHAR(MAX)'')
FROM Product
WHERE Product.ID=10;
Tenga en cuenta que esta solución sería una solución elegante pero menos eficiente que la tabla uno por separado. Lo cual puede estar bien para alguna aplicación.
No veo ninguna ventaja en el uso de las columnas XML para almacenar los valores localizados. Excepto quizás que tiene todas las versiones localizadas de un elemento "en un solo lugar" si eso le vale algo.
Propondría utilizar una columna cultureID en cada tabla que tenga elementos localizables. De esta forma, no necesita ningún manejo de XML en absoluto. Ya tiene sus datos en un esquema relacional, entonces, ¿por qué introducir otra capa de complejidad cuando el esquema relacional es perfectamente capaz de manejar el problema?
Digamos "sv-SE" tiene cultureID = 1 y "en-EN" tiene 2.
Entonces su consulta sería modificada como
SELECT *
From Product
Where Product.ID = 10 AND Product.cultureID = 1
para un cliente sueco
Esta solución que he visto con frecuencia en bases de datos localizadas. Se escala bien con el número de culturas y la cantidad de registros de datos. Evita el análisis y el procesamiento XML y es fácil de implementar.
Y otro punto: la solución XML le ofrece una flexibilidad que no necesita: podría, por ejemplo, tomar el valor "sv-SE" de la columna "Nombre" y el valor "en-EN" de " Descripción "-column. Sin embargo, no es necesario, ya que su cliente solo solicitará una cultura a la vez. La flexibilidad generalmente tiene un costo. En este caso, es necesario analizar todas las columnas individualmente, mientras que con la solución cultureID obtiene el registro completo con todos los valores correctos para la cultura solicitada.
Aquí algunos temas en el blog de Rick Strahl:
Localización de la base de datos Localización de JavaScript
Prefiero usar un solo conmutador en una tabla UserSetting, que se utiliza llamando al procedimiento almacenado ... aquí parte del código
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]
IR
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 @procStep=@procStep + ''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 u.DomainName=@domainUser)
set @guiLanguage = REPLACE ( @guiLanguage , ''-'' , ''_'' ) ;
--===============================================================
set @procStep=@procStep + '' BUILDING THE SQL QUERY ''
--===============================================================
DECLARE @sqlQuery AS nvarchar(2000)
SET @sqlQuery = ''SELECT MsgKey , MsgTxt FROM dbo.lang_'' + @guiLanguage + ''_Msg''
--===============================================================
set @procStep=@procStep + ''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
Veo el delima en general: tiene una sola entidad que debe representar como una sola instancia (un ProductID de "10", por ejemplo), pero tiene múltiples textos localizados de diferentes columnas / propiedades. Esa es una pregunta difícil, y veo la necesidad de sistemas POS, que solo quiera rastrear ese ProductID = 10, no múltiples productos que tienen diferentes ProductID, pero son lo mismo con solo texto diferente.
Me inclinaría por la solución de columna XML que usted y otros ya han descrito aquí. Sí, es más la transferencia de datos a través del cable, pero mantiene las cosas simples y se puede filtrar con XElement si el sitio del paquete se convierte en un problema.
El principal inconveniente es la cantidad de datos transferidos a través del cable desde el DB a la capa de servicio / UI / App. Intentaría realizar alguna transformación en el extremo SQL antes de devolver el resultado, para devolver solo la UI de una cultura. Siempre se puede simplemente SELECCIONAR el culsture currículum a través de xml en un sproc, y devolverlo como texto normal también.
En general, esto es diferente, por ejemplo, a una publicación de blog o necesidad de CMS para la localización, que he hecho algunas de ellas.
Mi enfoque para el Post scenerio sería similar al de TToni, con la excepción de modelar los datos desde la perspectiva del Dominio (y un toque de BDD). Dicho esto, concéntrate en lo que quieres lograr:
Given a users culture is "sv-se"
When the user views a post list
It should list posts only in "sv-se" culture
Esto significa que el usuario debería ver una lista de publicaciones solo para su cultura. La forma en que implementamos esto antes era pasar un conjunto de culturas para realizar consultas en función de lo que el usuario podía ver. Si el usuario tiene ''sv-se'' establecido como su principal, pero también ha seleccionado que hablan inglés de los Estados Unidos (en-us), entonces la consulta sería:
SELECT * FROM Post WHERE CultureUI IN (''sv-se'', ''en-us'')
Observe cómo esto le da todas las publicaciones y sus diferentes PostID, únicos para ese idioma. El PostID no es tan importante aquí en los blogs porque cada publicación está vinculada a un idioma diferente. Si hay copias que se transcriben, eso funciona bien aquí también, ya que cada publicación es única para esa cultura, y por lo tanto recibe un conjunto único de comentarios y tal.
Pero para volver a la primera parte de mi respuesta, su necesidad proviene del requisito de necesitar una sola instancia con múltiples textos. Una columna Xml se ajusta a eso bien.
Otro enfoque a considerar: no almacene el contenido en la base de datos, pero mantenga la "aplicación" apoyando los registros de la base de datos y el "contenido" como entidades separadas.
Usé un enfoque similar a esto cuando creé varios temas para mi sitio web de comercio electrónico. Varios de los productos tienen un logotipo de fabricante que también debe coincidir con el tema del sitio web. Como no existe un soporte de base de datos real para los temas, tuve un problema. La solución que se me ocurrió fue usar un token en la base de datos para identificar el ClientID de la imagen, en lugar de almacenar la URL de la imagen (que variaría según el tema).
Siguiendo el mismo enfoque, podría cambiar su base de datos de almacenar el nombre y la descripción del producto para almacenar un token de nombre y un token de descripción que identificaría el recurso (en un archivo resx o en la base de datos utilizando el enfoque de Rick Strahl) que contiene el contenido. La funcionalidad incorporada de .NET manejaría el cambio de idioma en lugar de intentar hacerlo en la base de datos (rara vez es una buena idea poner la lógica de negocios en la base de datos). Luego puede usar el token en el cliente para buscar el recurso correcto.
Label1.Text = GetLocalResourceObject("TokenStoredInDatabase").ToString()
La desventaja de este enfoque es, obviamente, mantener los tokens de la base de datos y los tokens de recursos sincronizados (porque los productos se pueden agregar sin ninguna descripción), pero podría hacerse más fácil utilizando un proveedor de recursos como el creado por Rick Strahl. Este enfoque puede no funcionar si tiene productos que cambian con frecuencia, pero para algunas personas podría serlo.
La ventaja es que tiene una pequeña cantidad de datos para transferir al cliente desde la base de datos, su contenido está claramente separado de su base de datos y su base de datos no tendrá que ser más compleja de lo que es ahora.
En una nota lateral, si está ejecutando una tienda de comercio electrónico y realmente desea indexar sus páginas localizadas, debe desviarse un poco de la forma aparentemente natural que creó Microsoft. Existe un claro desacuerdo entre un flujo de diseño práctico y lógico y lo que Google recomienda para SEO. De hecho, algunos webmasters se han quejado de que sus páginas no fueron indexadas por los motores de búsqueda para nada más que la cultura "predeterminada" porque los motores de búsqueda solo indexarán una sola URL incluso si cambia según la cultura del navegador.
Afortunadamente, hay un enfoque simple para evitar esto: poner enlaces en la página para traducirlo a los otros idiomas en función de un parámetro querystring. Se puede encontrar un ejemplo de esto (oops, ¡no me dejarán publicar otro enlace!) Y si lo comprueban, cada cultura de la página ha sido indexada tanto por Google como por Yahoo (aunque no por Bing). Un enfoque más avanzado puede usar la reescritura de URL en combinación con algunas expresiones regulares sofisticadas para hacer que su única página localizada parezca tener múltiples directorios, pero en realidad pase un parámetro de cadena de consulta a la página.
La indexación se convierte en un problema. No creo que pueda indexar xml, y por supuesto, no puede indexarlo si lo almacena como una cadena porque cada cadena comenzará con <localization> <text culture="...">
.
Así es como lo hice. No uso LINQ o SP para este, porque la consulta es demasiado compleja y está construida dinámicamente, y esto es solo un extracto de la consulta.
Tengo una tabla de productos:
* id
* price
* stocklevel
* active
* name
* shortdescription
* longdescription
y una tabla products_globalization:
* id
* products_id
* name
* shortdescription
* longdescription
Como puede ver, la tabla de productos también contiene todas las columnas de globalización. Estas columnas contienen el idioma predeterminado (por lo tanto, se puede omitir hacer una combinación al solicitar la cultura predeterminada) PERO no estoy seguro de si esto vale la pena, es decir, la unión entre las dos tablas está basada en el índice. .. - dame algunos comentarios sobre este).
Prefiero tener una tabla lado a lado sobre una tabla de recursos globales porque en ciertas situaciones podría necesitar hacer, por ejemplo, una base de datos (MySQL) MATCH en un par de columnas, como MATCH (nombre, shortdescription, longdescription) AGAINST ('' Algo aqui'').
En un escenario normal, pueden faltar algunas de las traducciones del producto, pero aún así quiero mostrar todos los productos (no solo los que están traducidos). Por lo tanto, no es suficiente para una combinación, de hecho necesitamos hacer una combinación a la izquierda basada en la tabla de productos.
Seudo:
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";
}
Me gustaría ir con COALESCE en lugar de CASE ELSE pero eso es además del punto.
Bueno, esa es mi opinión sobre eso. Siéntase libre de criticar mi sugerencia ...
Saludos cordiales, Richard