Este es un tema interesante, así que vamos a la nigromancia.
Comencemos por los problemas del método 1:
Problema: está desnormalizando para ahorrar velocidad.
En SQL (excepto PostGreSQL con hstore), no puede pasar un lenguaje de parámetros y decir:
SELECT ['DESCRIPTION_' + @in_language] FROM T_Products
Entonces tienes que hacer esto:
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
Lo que significa que debe modificar TODAS sus consultas si agrega un nuevo idioma. Naturalmente, esto lleva a usar "SQL dinámico", por lo que no tiene que alterar todas sus consultas.
Esto generalmente resulta en algo como esto (y no se puede usar en vistas o funciones con valores de tabla por cierto, lo que realmente es un problema si realmente necesita filtrar la fecha del informe)
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
El problema con esto es
a) El formato de fecha es muy específico del idioma, por lo que tiene un problema allí, si no ingresa en formato ISO (que el programador promedio de variedades de jardín generalmente no hace, y en caso de un informe que el usuario seguramente no hará por usted, incluso si se le indica explícitamente que lo haga).
y
b) lo más importante , se pierde cualquier tipo de comprobación de sintaxis . Si <insert name of your "favourite" person here>
altera el esquema porque de repente los requisitos para el cambio de alas, y se crea una nueva tabla, la anterior se fue pero se renombró el campo de referencia, no recibe ningún tipo de advertencia. Un informe incluso funciona cuando lo ejecuta sin seleccionar el parámetro de ala (==> guid.empty). Pero de repente, cuando un usuario real realmente selecciona un ala ==>auge . Este método infringe completamente cualquier tipo de prueba.
Método 2:
En pocas palabras: "Gran" idea (advertencia - sarcasmo), combinemos las desventajas del método 3 (velocidad lenta cuando hay muchas entradas) con las desventajas bastante horribles del método 1.
La única ventaja de este método es que usted mantiene todas las traducciones en una tabla y, por lo tanto, simplifican el mantenimiento Sin embargo, se puede lograr lo mismo con el método 1 y un procedimiento almacenado dinámico de SQL, y una tabla (posiblemente temporal) que contiene las traducciones, y el nombre de la tabla de destino (y es bastante simple asumiendo que usted nombró todos sus campos de texto) mismo).
Método 3:
una tabla para todas las traducciones: Desventaja: debe almacenar n claves foráneas en la tabla de productos para n campos que desea traducir. Por lo tanto, debe hacer n combinaciones para n campos. Cuando la tabla de traducción es global, tiene muchas entradas y las uniones se vuelven lentas. Además, siempre debe unirse a la tabla T_TRANSLATION n veces para n campos. Esto es un gran gasto. Ahora, ¿qué haces cuando debes acomodar traducciones personalizadas por cliente? Tendrá que agregar otras 2x n combinaciones en una tabla adicional. Si tiene que unirse, digamos 10 tablas, con 2x2xn = 4n combinaciones adicionales, ¡qué desastre! Además, este diseño permite utilizar la misma traducción con 2 tablas. Si cambio el nombre del elemento en una tabla, ¿realmente quiero cambiar también una entrada en otra tabla CADA VEZ?
Además, ya no puede eliminar y volver a insertar la tabla, porque ahora hay claves foráneas EN LA (S) TABLA (S) DEL PRODUCTO ... por supuesto, puede omitir la configuración de los FK y luego <insert name of your "favourite" person here>
puede eliminar la tabla y volver a insertarla todas las entradas con newid () [o especificando la identificación en la inserción, pero con la inserción de identidad desactivada ], y eso (y conducirá) a basura de datos (y excepciones de referencia nula) muy pronto.
Método 4 (no listado): Almacenar todos los idiomas en un campo XML en la base de datos. p.ej
-- 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&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
Luego puede obtener el valor mediante XPath-Query en SQL, donde puede colocar la variable de cadena en
filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla
Y puede actualizar el valor de esta manera:
UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with ""I am a ''value ""')
WHERE id = 1
Donde puedes reemplazar /lang/de/...
con'.../' + @in_language + '/...'
Al igual que el hstore de PostGre, excepto que debido a la sobrecarga de analizar XML (en lugar de leer una entrada de una matriz asociativa en PG hstore) se vuelve demasiado lento y la codificación xml hace que sea demasiado doloroso ser útil.
Método 5 (según lo recomendado por SunWuKung, el que debe elegir): una tabla de traducción para cada tabla de "Producto". Eso significa una fila por idioma y varios campos de "texto", por lo que solo se requiere UNA (izquierda) para unirse en N campos. Luego puede agregar fácilmente un campo predeterminado en la tabla "Producto", puede eliminar y volver a insertar fácilmente la tabla de traducción, y puede crear una segunda tabla para traducciones personalizadas (bajo demanda), que también puede eliminar y vuelva a insertar), y todavía tiene todas las claves foráneas.
Hagamos un ejemplo para ver esto FUNCIONA:
Primero, crea las tablas:
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
Luego complete los datos
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
Y luego consulta los datos:
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
Si eres perezoso, entonces también puedes usar el ISO-TwoLetterName ('DE', 'EN', etc.) como clave principal de la tabla de idiomas, entonces no tienes que buscar la identificación del idioma. Pero si lo hace, tal vez desee usar la etiqueta de idioma IETF , lo cual es mejor, porque obtiene de-CH y de-DE, que en realidad no es lo mismo en cuanto a ortografía (doble s en lugar de ß en todas partes) , aunque es el mismo lenguaje base. Es solo un pequeño detalle que puede ser importante para usted, especialmente teniendo en cuenta que en-US y en-GB / en-CA / en-AU o fr-FR / fr-CA tiene problemas similares.
Cita: no lo necesitamos, solo hacemos nuestro software en inglés.
Respuesta: Sí, pero ¿cuál?
De todos modos, si usa una ID de entero, es flexible y puede cambiar su método en cualquier momento posterior.
Y debe usar ese número entero, porque no hay nada más molesto, destructivo y problemático que un diseño Db fallido.
Ver también RFC 5646 , ISO 639-2 ,
Y, si todavía dice "nosotros" solo hacemos nuestra solicitud para "solo una cultura" (como en EE. UU. Por lo general), por lo tanto, no necesito ese número entero adicional, este sería un buen momento y lugar para mencionar el Etiquetas de idioma IANA , ¿no?
Porque van así:
de-DE-1901
de-DE-1996
y
de-CH-1901
de-CH-1996
(hubo una reforma ortográfica en 1996 ...) Intente encontrar una palabra en un diccionario si está mal escrita; Esto se vuelve muy importante en aplicaciones que tratan con portales legales y de servicio público.
Más importante aún, hay regiones que están cambiando de alfabetos cirílicos a latinos, lo que puede ser más problemático que la molestia superficial de alguna reforma de ortografía oscura, por lo que esto podría ser una consideración importante también, dependiendo del país en el que viva. De una forma u otra, es mejor tener ese número entero allí, por si acaso ...
Editar:
y agregando ON DELETE CASCADE
después
REFERENCES dbo.T_Products( PROD_Id )
simplemente puede decir: DELETE FROM T_Products
y no obtener ninguna violación de clave externa.
En cuanto a la recopilación, lo haría así:
A) Tenga su propio DAL
B) Guarde el nombre de intercalación deseado en la tabla de idiomas
Es posible que desee colocar las intercalaciones en su propia tabla, por ejemplo:
SELECT * FROM sys.fn_helpcollations()
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%'
C) Tenga el nombre de intercalación disponible en su información de idioma de usuario.
D) Escribe tu SQL así:
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE {#COLLATION}
E) Entonces, puedes hacer esto en tu DAL:
cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)
Lo que luego le dará esta consulta SQL perfectamente compuesta
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE German_PhoneBook_CI_AI