Ini adalah masalah yang menarik, jadi mari kita necromance.
Mari kita mulai dengan masalah metode 1:
Masalah: Anda melakukan denormalisasi untuk menghemat kecepatan.
Dalam SQL (kecuali PostGreSQL dengan hstore), Anda tidak dapat melewati bahasa parameter, dan mengatakan:
SELECT ['DESCRIPTION_' + @in_language] FROM T_Products
Jadi, Anda harus melakukan ini:
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
Yang berarti Anda harus mengubah SEMUA kueri Anda jika Anda menambahkan bahasa baru. Ini secara alami mengarah ke menggunakan "SQL dinamis", jadi Anda tidak perlu mengubah semua pertanyaan Anda.
Ini biasanya menghasilkan sesuatu seperti ini (dan itu tidak dapat digunakan dalam tampilan atau fungsi bernilai tabel dengan cara, yang benar-benar masalah jika Anda benar-benar perlu memfilter tanggal pelaporan)
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
Masalahnya adalah
a) Pemformatan tanggal sangat spesifik-bahasa, sehingga Anda mendapatkan masalah di sana, jika Anda tidak memasukkan dalam format ISO (yang biasanya tidak dilakukan oleh pemrogram varietas kebun, dan dalam kasus lapor pengguna yakin sekali tidak akan melakukan untuk Anda, bahkan jika secara eksplisit diinstruksikan untuk melakukannya).
dan
b) yang paling penting , Anda kehilangan segala jenis pemeriksaan sintaksis . Jika <insert name of your "favourite" person here>
mengubah skema karena tiba-tiba persyaratan untuk perubahan sayap, dan tabel baru dibuat, yang lama tersisa tetapi bidang referensi diganti namanya, Anda tidak mendapatkan peringatan apa pun. Laporan bahkan berfungsi ketika Anda menjalankannya tanpa memilih parameter sayap (==> guid.empty). Tapi tiba-tiba, ketika pengguna yang sebenarnya benar-benar memilih sayap ==>booming . Metode ini benar-benar memecah segala jenis pengujian.
Metode 2:
Singkatnya: "Hebat" ide (peringatan - sarkasme), mari kita gabungkan kelemahan metode 3 (kecepatan lambat ketika banyak entri) dengan kerugian metode yang agak mengerikan 1.
Satu-satunya keuntungan dari metode ini adalah Anda tetap menggunakan semua terjemahan dalam satu tabel, dan karenanya mempermudah pemeliharaan. Namun, hal yang sama dapat dicapai dengan metode 1 dan prosedur tersimpan SQL dinamis, dan tabel (mungkin sementara) yang berisi terjemahan, dan nama tabel target (dan cukup sederhana dengan asumsi Anda memberi nama semua bidang teks Anda sama).
Metode 3:
Satu tabel untuk semua terjemahan: Kerugian: Anda harus menyimpan dan Kunci Asing di tabel produk untuk bidang yang ingin Anda terjemahkan. Oleh karena itu, Anda harus melakukan n bergabung untuk bidang n. Ketika tabel terjemahan bersifat global, ia memiliki banyak entri, dan bergabung menjadi lambat. Selain itu, Anda selalu harus bergabung dengan tabel T_TRANSLATION n kali untuk n bidang. Ini cukup mahal. Sekarang, apa yang Anda lakukan ketika Anda harus mengakomodasi terjemahan khusus per pelanggan? Anda harus menambahkan 2 x n bergabung ke tabel tambahan. Jika Anda harus bergabung, katakan 10 tabel, dengan 2x2xn = 4n tambahan bergabung, sungguh berantakan! Selain itu, desain ini memungkinkan untuk menggunakan terjemahan yang sama dengan 2 tabel. Jika saya mengubah nama item dalam satu tabel, apakah saya benar-benar ingin mengubah entri di tabel lain juga SETIAP WAKTU TUNGGAL?
Plus, Anda tidak dapat menghapus dan menyisipkan lagi tabel, karena sekarang ada kunci asing di TABEL PRODUK ... Anda tentu saja dapat menghilangkan pengaturan FK, dan kemudian <insert name of your "favourite" person here>
dapat menghapus tabel, dan memasukkan kembali semua entri dengan newid () [atau dengan menentukan id di sisipan, tetapi memiliki identitas-sisipkan OFF ], dan itu akan (dan akan) menyebabkan data-sampah (dan pengecualian referensi-nol) sangat cepat.
Metode 4 (tidak terdaftar): Menyimpan semua bahasa dalam bidang XML dalam database. misalnya
-- 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
Kemudian Anda bisa mendapatkan nilai dengan XPath-Query di SQL, di mana Anda bisa memasukkan variabel-string
filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla
Dan Anda dapat memperbarui nilai seperti ini:
UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with ""I am a ''value ""')
WHERE id = 1
Di mana Anda dapat menggantinya /lang/de/...
dengan'.../' + @in_language + '/...'
Jenis seperti PostGre hstore, kecuali bahwa karena overhead parsing XML (alih-alih membaca entri dari array asosiatif di PG hstore) itu menjadi terlalu lambat ditambah pengkodean xml membuatnya terlalu menyakitkan untuk berguna.
Metode 5 (seperti yang direkomendasikan oleh SunWuKung, yang harus Anda pilih): Satu tabel terjemahan untuk setiap tabel "Produk". Itu berarti satu baris per bahasa, dan beberapa bidang "teks", sehingga hanya membutuhkan SATU (kiri) bergabung di bidang N. Kemudian Anda dapat dengan mudah menambahkan bidang default di tabel "Produk", Anda dapat dengan mudah menghapus dan memasukkan kembali tabel terjemahan, dan Anda dapat membuat tabel kedua untuk terjemahan khusus (sesuai permintaan), yang juga dapat Anda hapus dan masukkan kembali), dan Anda masih memiliki semua kunci asing.
Mari kita buat contoh untuk melihat KARYA ini:
Pertama, buat tabel:
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
Lalu isi data
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
Dan kemudian meminta data:
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
Jika Anda malas, maka Anda juga dapat menggunakan ISO-TwoLetterName ('DE', 'EN', dll.) Sebagai kunci utama tabel bahasa, maka Anda tidak perlu mencari id bahasa. Tetapi jika Anda melakukannya, Anda mungkin ingin menggunakan tag bahasa IETF sebagai gantinya, yang lebih baik, karena Anda mendapatkan de-CH dan de-DE, yang benar-benar tidak sama ortografi-bijaksana (double s bukannya ß di mana-mana) , meskipun bahasa dasarnya sama. Itu hanya detail kecil yang mungkin penting bagi Anda, terutama mengingat bahwa en-US dan en-GB / en-CA / en-AU atau fr-FR / fr-CA memiliki masalah yang sama.
Quote: kita tidak membutuhkannya, kita hanya melakukan perangkat lunak kita dalam bahasa Inggris
Jawab: Ya - tapi yang mana ??
Bagaimanapun, jika Anda menggunakan ID integer, Anda fleksibel, dan dapat mengubah metode Anda nanti.
Dan Anda harus menggunakan integer itu, karena tidak ada yang lebih menyebalkan, destruktif, dan merepotkan daripada desain Db yang gagal.
Lihat juga RFC 5646 , ISO 639-2 ,
Dan, jika Anda masih mengatakan "kami" hanya membuat aplikasi kami untuk "hanya satu budaya" (seperti biasanya di AS) - karena itu saya tidak memerlukan integer tambahan, ini akan menjadi waktu dan tempat yang tepat untuk menyebutkan Tag bahasa IANA , bukan?
Karena mereka pergi seperti ini:
de-DE-1901
de-DE-1996
dan
de-CH-1901
de-CH-1996
(ada reformasi ortografi pada tahun 1996 ...) Cobalah menemukan kata dalam kamus jika salah eja; ini menjadi sangat penting dalam aplikasi yang berhubungan dengan portal layanan hukum dan publik.
Lebih penting lagi, ada daerah yang berubah dari huruf kecil ke huruf latin, yang mungkin lebih merepotkan daripada gangguan dangkal dari beberapa reformasi ortografi yang tidak jelas, yang mengapa ini mungkin menjadi pertimbangan penting juga, tergantung pada negara tempat Anda tinggal. Dengan satu atau lain cara, lebih baik untuk memiliki integer di sana, untuk berjaga-jaga ...
Sunting:
Dan dengan menambahkan ON DELETE CASCADE
setelah
REFERENCES dbo.T_Products( PROD_Id )
Anda bisa mengatakan:, DELETE FROM T_Products
dan tidak mendapatkan pelanggaran kunci asing.
Adapun pemeriksaan, saya akan melakukannya seperti ini:
A) Memiliki DAL Anda sendiri
B) Simpan nama kolasi yang diinginkan dalam tabel bahasa
Anda mungkin ingin meletakkan koleksi di meja mereka sendiri, misalnya:
SELECT * FROM sys.fn_helpcollations()
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%'
C) Siapkan nama collation di informasi auth.user.language Anda
D) Tulis SQL Anda seperti ini:
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE {#COLLATION}
E) Kemudian, Anda dapat melakukan ini di DAL Anda:
cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)
Yang kemudian akan memberi Anda SQL-Query yang disusun dengan sempurna ini
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE German_PhoneBook_CI_AI