Questo è un problema interessante, quindi andiamo a negrance.
Cominciamo dai problemi del metodo 1:
Problema: stai denormalizzando per risparmiare velocità.
In SQL (tranne PostGreSQL con hstore), non puoi passare un linguaggio di parametri e dire:
SELECT ['DESCRIPTION_' + @in_language] FROM T_Products
Quindi devi fare questo:
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
Ciò significa che devi modificare TUTTE le tue query se aggiungi una nuova lingua. Ciò porta naturalmente all'utilizzo di "SQL dinamico", quindi non è necessario modificare tutte le query.
Questo di solito si traduce in qualcosa del genere (e non può essere utilizzato nelle viste o nelle funzioni con valori di tabella a proposito, il che è davvero un problema se devi effettivamente filtrare la data del rapporto)
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
Il problema con questo è
a) La formattazione della data è molto specifica della lingua, quindi c'è un problema lì, se non si inserisce in formato ISO (cosa che di solito non fa il programmatore medio di varietà da giardino, e in caso di un rapporto che l'utente sicuramente non farà per te, anche se esplicitamente incaricato di farlo).
e
b) soprattutto , perdi qualsiasi tipo di controllo della sintassi . Se <insert name of your "favourite" person here>
altera lo schema perché improvvisamente i requisiti per l'ala cambiano e viene creata una nuova tabella, quella vecchia è rimasta ma il campo di riferimento è stato rinominato, non si riceve alcun tipo di avviso. Un report funziona anche quando lo si esegue senza selezionare il parametro wing (==> guid.empty). Ma improvvisamente, quando un utente reale seleziona effettivamente un'ala ==> boom . Questo metodo interrompe completamente qualsiasi tipo di test.
Metodo 2:
In poche parole: "Ottima" idea (avvertenza - sarcasmo), combiniamo gli svantaggi del metodo 3 (bassa velocità quando molte voci) con gli svantaggi piuttosto orribili del metodo 1.
L'unico vantaggio di questo metodo è che si mantiene tutte le traduzioni in una tabella e quindi semplificano la manutenzione. Tuttavia, la stessa cosa può essere raggiunta con il metodo 1 e una stored procedure SQL dinamica e una tabella (possibilmente temporanea) contenente le traduzioni e il nome della tabella di destinazione (ed è abbastanza semplice supponendo che tu abbia chiamato tutti i tuoi campi di testo il stesso).
Metodo 3:
una tabella per tutte le traduzioni: Svantaggio: è necessario memorizzare n chiavi esterne nella tabella dei prodotti per n campi che si desidera tradurre. Pertanto, devi fare n join per n campi. Quando la tabella di traduzione è globale, ha molte voci e i join diventano lenti. Inoltre, devi sempre unirti alla tabella T_TRANSLATION n volte per n campi. Questo è piuttosto un sovraccarico. Ora, cosa fai quando devi inserire traduzioni personalizzate per cliente? Dovrai aggiungere altri 2x n join su una tabella aggiuntiva. Se devi unirti, diciamo 10 tavoli, con 2x2xn = 4n join aggiuntivi, che casino! Inoltre, questo design consente di utilizzare la stessa traduzione con 2 tabelle. Se cambio il nome dell'articolo in una tabella, voglio davvero cambiare anche una voce in un'altra tabella OGNI SINGOLO?
Inoltre non puoi più cancellare e reinserire la tabella, perché ora ci sono chiavi esterne nella TABELLA (E) DEL PRODOTTO ... puoi ovviamente omettere di impostare gli FK, quindi <insert name of your "favourite" person here>
puoi eliminare la tabella e reinserire tutte le voci con newid () [o specificando l'id nell'insert, ma con identità-insert OFF ], e ciò porterebbe (e porterebbe) a dati-garbage (e eccezioni riferimento null) molto presto.
Metodo 4 (non elencato): memorizzazione di tutte le lingue in un campo XML nel database. per esempio
-- 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
Quindi è possibile ottenere il valore da XPath-Query in SQL, dove è possibile inserire la variabile stringa
filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla
E puoi aggiornare il valore in questo modo:
UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with ""I am a ''value ""')
WHERE id = 1
Dove puoi sostituirlo /lang/de/...
con'.../' + @in_language + '/...'
Un po 'come l'hstore di PostGre, tranne che a causa del sovraccarico di analizzare XML (invece di leggere una voce da un array associativo in PG hstore) diventa troppo lento più la codifica XML rende troppo doloroso essere utile.
Metodo 5 (come raccomandato da SunWuKung, quello che dovresti scegliere): una tabella di traduzione per ogni tabella "Prodotto". Ciò significa una riga per lingua e diversi campi "di testo", quindi richiede solo UN (a sinistra) join su N campi. Quindi puoi facilmente aggiungere un campo predefinito nella tabella "Prodotto", puoi eliminare e reinserire facilmente la tabella di traduzione e puoi creare una seconda tabella per traduzioni personalizzate (su richiesta), che puoi anche eliminare e reinserire) e hai ancora tutte le chiavi esterne.
Facciamo un esempio per vedere questo LAVORI:
Innanzitutto, crea le tabelle:
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
Quindi inserire i dati
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 quindi interrogare i dati:
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 sei pigro, puoi anche usare ISO-TwoLetterName ('DE', 'EN', ecc.) Come chiave primaria della tabella delle lingue, quindi non devi cercare l'ID della lingua. Ma se lo fai, potresti voler usare invece il tag del linguaggio IETF , il che è meglio, perché ottieni de-CH e de-DE, che non è davvero la stessa ortografia (doppie s invece di ß ovunque) , sebbene sia lo stesso linguaggio di base. Questo è solo un piccolo dettaglio che può essere importante per te, specialmente considerando che en-US / en-GB / en-CA / en-AU o fr-FR / fr-CA hanno problemi simili.
Citazione: non ne abbiamo bisogno, facciamo il nostro software solo in inglese.
Risposta: Sì, ma quale ??
Ad ogni modo, se usi un ID intero, sei flessibile e puoi cambiare metodo in qualsiasi momento successivo.
E dovresti usare quell'intero, perché non c'è niente di più fastidioso, distruttivo e problematico di un design Db pasticciato.
Vedi anche RFC 5646 , ISO 639-2 ,
E, se si sta ancora dicendo "noi" solo facciamo la nostra domanda di "solo una cultura" (come en-US di solito) - quindi non ho bisogno che interi in più, questo sarebbe un buon momento e il luogo per parlare del Tag di lingua IANA , no?
Perché vanno così:
de-DE-1901
de-DE-1996
e
de-CH-1901
de-CH-1996
(c'è stata una riforma dell'ortografia nel 1996 ...) Prova a trovare una parola in un dizionario se è scritta male; questo diventa molto importante nelle applicazioni che si occupano di portali legali e di servizio pubblico.
Ancora più importante, ci sono regioni che stanno cambiando da alfabeti cirillici a latini, che potrebbero essere solo più fastidiosi del fastidio superficiale di qualche oscura riforma dell'ortografia, motivo per cui questa potrebbe essere anche una considerazione importante, a seconda del paese in cui vivi. In un modo o nell'altro, è meglio avere quell'intero lì dentro, nel caso in cui ...
Modifica:
e aggiungendo ON DELETE CASCADE
dopo
REFERENCES dbo.T_Products( PROD_Id )
puoi semplicemente dire: DELETE FROM T_Products
e non ottenere alcuna violazione di chiave esterna.
Per quanto riguarda la collazione, lo farei così:
A) Avere il proprio DAL
B) Salvare il nome della fascicolazione desiderata nella tabella della lingua
Potresti voler mettere le regole di confronto nella loro tabella, ad esempio:
SELECT * FROM sys.fn_helpcollations()
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%'
C) Avere il nome della collazione disponibile nelle informazioni auth.user.language
D) Scrivi il tuo SQL in questo modo:
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE {#COLLATION}
E) Quindi, puoi farlo nel tuo DAL:
cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)
Che ti darà quindi questa query SQL perfettamente 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