C'est un problème intéressant, alors nécromancions.
Commençons par les problèmes de la méthode 1:
Problème: vous dénormalisez pour gagner en vitesse.
En SQL (sauf PostGreSQL avec hstore), vous ne pouvez pas passer un langage de paramètres et dire:
SELECT ['DESCRIPTION_' + @in_language] FROM T_Products
Vous devez donc faire ceci:
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
Ce qui signifie que vous devez modifier TOUTES vos requêtes si vous ajoutez une nouvelle langue. Cela conduit naturellement à utiliser du "SQL dynamique", vous n'avez donc pas à modifier toutes vos requêtes.
Cela se traduit généralement par quelque chose comme ça (et il ne peut pas être utilisé dans les vues ou les fonctions table par ailleurs, ce qui est vraiment un problème si vous avez réellement besoin de filtrer la date de rapport)
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
Le problème avec ceci est
a) Le formatage de la date est très spécifique à la langue, donc vous obtenez un problème là-bas, si vous n'entrez pas au format ISO (ce que le programmeur de jardin moyen ne fait généralement pas, et en cas de un rapport que l'utilisateur sûr que l'enfer ne fera pas pour vous, même s'il est explicitement chargé de le faire).
et
b) plus important encore , vous perdez tout type de vérification de syntaxe . Si <insert name of your "favourite" person here>
modifie le schéma parce que soudainement, les exigences de changement d'aile et qu'une nouvelle table est créée, l'ancienne est partie mais le champ de référence a été renommé, vous n'avez aucun avertissement. Un rapport fonctionne même lorsque vous l'exécutez sans sélectionner le paramètre wing (==> guid.empty). Mais soudain, quand un utilisateur réel sélectionne réellement une aile ==>boom . Cette méthode rompt complètement tout type de test.
Méthode 2:
En résumé: "Grande" idée (avertissement - sarcasme), combinons les inconvénients de la méthode 3 (vitesse lente avec de nombreuses entrées) aux inconvénients plutôt horribles de la méthode 1.
Le seul avantage de cette méthode est que vous gardez toutes les traductions dans une seule table, et donc la maintenance est simple. Cependant, la même chose peut être obtenue avec la méthode 1 et une procédure stockée SQL dynamique, et une table (éventuellement temporaire) contenant les traductions et le nom de la table cible (et est assez simple en supposant que vous avez nommé tous vos champs de texte le même).
Méthode 3:
une table pour toutes les traductions: Inconvénient: vous devez stocker n clés étrangères dans la table des produits pour n champs que vous souhaitez traduire. Par conséquent, vous devez effectuer n jointures pour n champs. Lorsque la table de traduction est globale, elle comporte de nombreuses entrées et les jointures deviennent lentes. De plus, vous devez toujours joindre la table T_TRANSLATION n fois pour n champs. C'est tout à fait une surcharge. Maintenant, que faites-vous lorsque vous devez accepter des traductions personnalisées par client? Vous devrez ajouter encore 2x n jointures sur une table supplémentaire. Si vous devez rejoindre, disons 10 tables, avec 2x2xn = 4n jointures supplémentaires, quel gâchis! De plus, cette conception permet d'utiliser la même traduction avec 2 tableaux. Si je change le nom d'un élément dans une table, est-ce que je veux vraiment changer une entrée dans une autre table aussi CHAQUE FOIS?
De plus, vous ne pouvez plus supprimer et réinsérer la table, car il y a maintenant des clés étrangères DANS LES TABLEAUX DE PRODUITS ... vous pouvez bien sûr omettre de définir les FK, puis <insert name of your "favourite" person here>
supprimer la table et réinsérer toutes les entrées avec newid () [ou en spécifiant l'id dans l'insertion, mais ayant l' identité-insertion désactivée ], et cela conduirait (et entraînera) très rapidement des données-garbage (et des exceptions de référence nulle).
Méthode 4 (non répertoriée): stockage de toutes les langues dans un champ XML de la base de données. par exemple
-- 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
Ensuite, vous pouvez obtenir la valeur par XPath-Query en SQL, où vous pouvez mettre la variable de chaîne dans
filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla
Et vous pouvez mettre à jour la valeur comme ceci:
UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with ""I am a ''value ""')
WHERE id = 1
Où vous pouvez remplacer /lang/de/...
par'.../' + @in_language + '/...'
Un peu comme l'hstore PostGre, sauf qu'en raison de la surcharge d'analyse XML (au lieu de lire une entrée d'un tableau associatif dans PG hstore), il devient beaucoup trop lent et l'encodage xml le rend trop pénible pour être utile.
Méthode 5 (comme recommandé par SunWuKung, celle que vous devez choisir): Une table de traduction pour chaque table "Produit". Cela signifie une ligne par langue et plusieurs champs "texte", il ne nécessite donc qu'une seule jointure (gauche) sur N champs. Ensuite, vous pouvez facilement ajouter un champ par défaut dans la table "Produit", vous pouvez facilement supprimer et réinsérer la table de traduction, et vous pouvez créer une deuxième table pour les traductions personnalisées (sur demande), que vous pouvez également supprimer et réinsérez), et vous avez toujours toutes les clés étrangères.
Faisons un exemple pour voir ce TRAVAIL:
Créez d'abord les tableaux:
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
Remplissez ensuite les données
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
Et puis interrogez les données:
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 vous êtes paresseux, vous pouvez également utiliser l'ISO-TwoLetterName ('DE', 'EN', etc.) comme clé primaire de la table des langues, vous n'avez donc pas à rechercher l'ID de la langue. Mais si vous le faites, vous voudrez peut-être utiliser la balise de langue IETF à la place, ce qui est mieux, car vous obtenez de-CH et de-DE, ce qui n'est vraiment pas la même chose en termes d'ortographie (double s au lieu de ß partout) , bien qu'il s'agisse du même langage de base. C'est un tout petit détail qui peut être important pour vous, surtout si l'on considère que en-US et en-GB / en-CA / en-AU ou fr-FR / fr-CA ont des problèmes similaires.
Quote: nous n'en avons pas besoin, nous ne faisons que notre logiciel en anglais.
Réponse: Oui - mais lequel ??
Quoi qu'il en soit, si vous utilisez un ID entier, vous êtes flexible et pouvez modifier votre méthode à tout moment.
Et vous devez utiliser cet entier, car il n'y a rien de plus ennuyeux, destructeur et gênant qu'une conception Db bâclée.
Voir aussi RFC 5646 , ISO 639-2 ,
Et, si vous dites toujours "nous" ne faisons notre demande que pour " une seule culture" (comme en-US en général) - donc je n'ai pas besoin de cet entier supplémentaire, ce serait le bon moment et l'endroit pour mentionner le Les balises de langue IANA , n'est-ce pas?
Parce qu'ils vont comme ça:
de-DE-1901
de-DE-1996
et
de-CH-1901
de-CH-1996
(il y a eu une réforme de l'orthographe en 1996 ...) Essayez de trouver un mot dans un dictionnaire s'il est mal orthographié; cela devient très important dans les applications traitant des portails juridiques et de service public.
Plus important encore, certaines régions passent de l'alphabet cyrillique à l'alphabet latin, ce qui peut être plus gênant que la nuisance superficielle d'une réforme obscure de l'orthographe, c'est pourquoi cela pourrait également être un facteur important, selon le pays dans lequel vous vivez. D'une façon ou d'une autre, il vaut mieux avoir cet entier là, juste au cas où ...
Edit:
Et en ajoutant ON DELETE CASCADE
après
REFERENCES dbo.T_Products( PROD_Id )
vous pouvez simplement dire: DELETE FROM T_Products
et n'obtenir aucune violation de clé étrangère.
Quant au classement, je le ferais comme ceci:
A) Disposez de votre propre DAL
B) Enregistrez le nom du classement souhaité dans la table des langues
Vous voudrez peut-être placer les classements dans leur propre tableau, par exemple:
SELECT * FROM sys.fn_helpcollations()
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%'
C) Ayez le nom du classement disponible dans vos informations auth.user.language
D) Écrivez votre SQL comme ceci:
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE {#COLLATION}
E) Ensuite, vous pouvez le faire dans votre DAL:
cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)
Ce qui vous donnera alors cette requête SQL parfaitement composée
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE German_PhoneBook_CI_AI