Esta é uma questão interessante, então vamos necromance.
Vamos começar pelos problemas do método 1:
Problema: você está desnormalizando para economizar velocidade.
No SQL (exceto PostGreSQL com hstore), você não pode passar uma linguagem de parâmetro e dizer:
SELECT ['DESCRIPTION_' + @in_language] FROM T_Products
Então você tem que fazer isso:
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
O que significa que você deve alterar TODAS as suas consultas se adicionar um novo idioma. Isso naturalmente leva ao uso de "SQL dinâmico", para que você não precise alterar todas as suas consultas.
Isso geralmente resulta em algo assim (e não pode ser usado em exibições ou funções com valor de tabela, a propósito, o que realmente é um problema se você realmente precisar filtrar a data do relatório)
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
O problema é que:
a formatação da data é muito específica do idioma; portanto, você encontra um problema se não inserir no formato ISO (o que o programador comum de variedades de jardins geralmente não faz e, no caso de um relatório que o usuário com certeza não fará por você, mesmo que seja explicitamente instruído a fazê-lo).
e
b) de forma mais significativa , você perder qualquer tipo de verificação de sintaxe . Se <insert name of your "favourite" person here>
alterar o esquema, porque de repente os requisitos para alteração de ala e uma nova tabela é criada, a antiga foi deixada, mas o campo de referência foi renomeado, você não recebe nenhum tipo de aviso. Um relatório até funciona quando você o executa sem selecionar o parâmetro wing (==> guid.empty). Mas de repente, quando um usuário real realmente seleciona uma asa ==>boom . Esse método viola completamente qualquer tipo de teste.
Método 2:
Em poucas palavras: idéia "ótima" (aviso - sarcasmo), vamos combinar as desvantagens do método 3 (velocidade lenta em muitas entradas) com as horríveis desvantagens do método 1.
A única vantagem desse método é que você mantém toda a tradução em uma tabela e, portanto, simplifica a manutenção. No entanto, o mesmo pode ser alcançado com o método 1 e um procedimento dinâmico armazenado em SQL, além de uma tabela (possivelmente temporária) contendo as traduções e o nome da tabela de destino (e é bastante simples, assumindo que você nomeou todos os seus campos de texto como mesmo).
Método 3:
Uma tabela para todas as traduções: Desvantagem: você precisa armazenar n Chaves estrangeiras na tabela de produtos para os n campos que deseja traduzir. Portanto, você precisa fazer n junções para n campos. Quando a tabela de conversão é global, ela possui muitas entradas e as junções ficam lentas. Além disso, você sempre deve ingressar na tabela T_TRANSLATION n vezes para n campos. Isso é uma sobrecarga. Agora, o que você faz quando precisa acomodar traduções personalizadas por cliente? Você precisará adicionar outras junções 2x n em uma tabela adicional. Se você tiver que entrar, digamos 10 mesas, com 2x2xn = 4n junções adicionais, que confusão! Além disso, esse design torna possível usar a mesma tradução com 2 tabelas. Se eu alterar o nome do item em uma tabela, eu realmente quero alterar uma entrada em outra tabela também A CADA VEZ?
Além disso, você não pode mais excluir e reinserir a tabela, porque agora existem chaves estrangeiras NA (s) TABELA (S) DE PRODUTO (s) ... é claro que você pode omitir a configuração dos FKs e, em seguida, <insert name of your "favourite" person here>
pode excluir a tabela e reinserir todas as entradas com newid () [ou especificando o ID na inserção, mas com a inserção de identidade desativada ], e isso levaria (e levará) a lixo de dados (e exceções de referência nula) muito em breve.
Método 4 (não listado): Armazenando todos os idiomas em um campo XML no banco de dados. por exemplo
-- 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
Em seguida, você pode obter o valor por XPath-Query no SQL, onde você pode colocar a variável de cadeia em
filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla
E você pode atualizar o valor assim:
UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with ""I am a ''value ""')
WHERE id = 1
Onde você pode substituir /lang/de/...
por'.../' + @in_language + '/...'
Assim como o hstore PostGre, exceto que, devido à sobrecarga de analisar XML (em vez de ler uma entrada de uma matriz associativa no PG hstore), ele fica muito lento, mais a codificação xml torna muito doloroso ser útil.
Método 5 (conforme recomendado pelo SunWuKung, o que você deve escolher): Uma tabela de conversão para cada tabela "Produto". Isso significa uma linha por idioma e vários campos de "texto", portanto, requer apenas UMA (esquerda) junção em N campos. Em seguida, você pode adicionar facilmente um campo padrão na tabela "Produto", excluir e reinserir facilmente a tabela de conversão e criar uma segunda tabela para traduções personalizadas (sob demanda), que também podem ser excluídas. e reinsira), e você ainda terá todas as chaves estrangeiras.
Vamos fazer um exemplo para ver isso FUNCIONA:
Primeiro, crie as tabelas:
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
Em seguida, preencha os dados
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
E, em seguida, consulte os dados:
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
Se você é preguiçoso, também pode usar o ISO-TwoLetterName ('DE', 'EN' etc.) como chave primária da tabela de idiomas, e não precisa procurar o ID do idioma. Mas se você fizer isso, talvez queira usar a tag no idioma IETF , o que é melhor, porque você obtém de-CH e de-DE, que não são realmente os mesmos em termos de ortografia (s duplo em vez de ß em todos os lugares) , embora seja o mesmo idioma base. Isso é apenas um pequeno detalhe que pode ser importante para você, especialmente considerando que en-US e en-GB / en-CA / en-AU ou fr-FR / fr-CA têm problemas semelhantes.
Citação: não precisamos, fazemos apenas nosso software em inglês.
Resposta: Sim - mas qual?
De qualquer forma, se você usa um número inteiro, é flexível e pode alterar seu método posteriormente.
E você deve usar esse número inteiro, porque não há nada mais irritante, destrutivo e problemático do que um design de Db danificado.
Veja também RFC 5646 , ISO 639-2 ,
E, se você ainda está dizendo "nós" apenas fazemos nosso pedido para "apenas uma cultura" (como geralmente nos EUA) - portanto, não preciso desse número inteiro extra, esse seria um bom momento e um lugar para mencionar o Tags de idioma da IANA , não?
Porque eles são assim:
de-DE-1901
de-DE-1996
e
de-CH-1901
de-CH-1996
(houve uma reforma ortográfica em 1996 ...) Tente encontrar uma palavra em um dicionário se ela estiver incorreta; isso se torna muito importante em aplicativos que lidam com portais legais e de serviço público.
Mais importante, existem regiões que estão mudando de alfabetos cirílico para latino, o que pode ser mais problemático do que o incômodo superficial de alguma reforma obscura da ortografia, e é por isso que isso também pode ser uma consideração importante, dependendo do país em que você vive. De uma forma ou de outra, é melhor ter esse número inteiro lá, por via das dúvidas ...
Edit:
E adicionando ON DELETE CASCADE
depois
REFERENCES dbo.T_Products( PROD_Id )
você pode simplesmente dizer: DELETE FROM T_Products
e não obter nenhuma violação de chave estrangeira.
Quanto ao agrupamento, eu faria assim:
A) Tenha seu próprio DAL
B) Salve o nome do agrupamento desejado na tabela de idiomas
Você pode colocar os agrupamentos em sua própria tabela, por exemplo:
SELECT * FROM sys.fn_helpcollations()
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%'
C) Tenha o nome do agrupamento disponível em suas informações de auth.user.language
D) Escreva seu SQL assim:
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE {#COLLATION}
E) Em seguida, você pode fazer isso no seu DAL:
cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)
O que lhe dará essa consulta SQL perfeitamente composta
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE German_PhoneBook_CI_AI