이것은 흥미로운 문제이므로, 괴롭히자.
방법 1의 문제부터 시작하겠습니다.
문제 : 속도를 절약하기 위해 비정규 화하고 있습니다.
SQL (hstore가있는 PostGreSQL 제외)에서는 매개 변수 언어를 전달할 수 없으며 다음과 같이 말합니다.
SELECT ['DESCRIPTION_' + @in_language] FROM T_Products
따라서이 작업을 수행해야합니다.
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
새 언어를 추가하면 모든 쿼리를 변경해야합니다. 자연스럽게 "동적 SQL"을 사용하므로 모든 쿼리를 변경할 필요가 없습니다.
이것은 일반적으로 이와 같은 결과를 낳습니다 (그리고 뷰 또는 테이블 반환 함수에서는 사용할 수 없습니다. 실제로보고 날짜를 필터링 해야하는 경우 실제로 문제가됩니다)
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
이것에 대한 문제는
a) 날짜 형식은 언어마다 매우 다르므로 ISO 형식으로 입력하지 않으면 문제가 발생합니다 (일반 정원 다양성 프로그래머가 일반적으로하지 않는 경우). 명시 적으로 지시 한 경우에도 사용자가 대체하지 않을 것임을 사용자에게 확실하게보고합니다.
그리고
b) 가장 중요한 것은 모든 종류의 구문 검사 를 푼다는 것입니다 . 경우 <insert name of your "favourite" person here>
변경합니다 갑자기 날개 변화에 대한 요구 사항 및 AA 새 테이블이 만들어지기 때문에 스키마가 이전 하나가 남아 있지만, 참조 필드는 이름, 당신은 경고의 어떤 종류를하지 않습니다. 날개 매개 변수 (==> guid.empty) 를 선택하지 않고 보고서 를 실행할 때도 보고서가 작동합니다 . 그러나 갑자기 실제 사용자가 실제로 날개를 선택하면 ==>붐 . 이 방법은 모든 종류의 테스트를 완전히 중단합니다.
방법 2 :
간단히 말해서 : "훌륭한"아이디어 (경고-풍자), 방법 3의 단점 (많은 항목이있을 때 느린 속도)을 방법 1
의 다소 끔찍한 단점과 결합 해 봅시다. 이 방법
의 유일한 장점은 하나의 테이블로 모든 번역을 수행하므로 유지 관리가 간단합니다. 그러나 방법 1과 동적 SQL 저장 프로 시저, 그리고 번역을 포함하는 (일시적인) 테이블 및 대상 테이블의 이름을 사용하여 동일한 작업을 수행 할 수 있습니다 (모든 텍스트 필드의 이름을 가정하면 매우 간단합니다). 같은).
방법 3 :
모든 번역에 대한 하나의 테이블 : 단점 : 번역하려는 n 개의 필드에 대해 제품 테이블에 n 개의 외래 키를 저장해야합니다. 따라서 n 개의 필드에 대해 n 개의 조인을 수행해야합니다. 변환 표가 전역 테이블이면 많은 항목이 있으며 결합이 느려집니다. 또한 항상 n 개의 필드에 대해 T_TRANSLATION 테이블을 n 번 조인해야합니다. 이것은 상당히 오버 헤드입니다. 이제 고객 당 사용자 정의 번역을 수용해야 할 경우 어떻게해야합니까? 추가 테이블에 다른 2x n 조인을 추가해야합니다. 2x2xn = 4n 추가 조인으로 10 개의 테이블을 조인 해야하는 경우 얼마나 엉망입니까! 또한이 디자인을 사용하면 2 개의 테이블에서 동일한 변환을 사용할 수 있습니다. 한 테이블에서 항목 이름을 변경하면 매번 다른 테이블에서도 항목을 변경하고 싶습니까?
또한 이제 제품 테이블에 외래 키가 있으므로 더 이상 테이블을 삭제하고 다시 삽입 할 수 없습니다 ... 물론 FK 설정을 생략 한 다음 <insert name of your "favourite" person here>
테이블을 삭제하고 다시 삽입 할 수 있습니다 newid () 또는 삽입에서 ID를 지정하지만 identity-insert가 OFF 인 모든 항목은 데이터 가비지 (및 널 참조 예외)로 곧 이어질 것입니다.
방법 4 (나열되지 않음) : 모든 언어를 데이터베이스의 XML 필드에 저장 예 :
-- 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
그런 다음 SQL에서 XPath-Query로 값을 얻을 수 있습니다. 여기서 string-variable을
filemeta.value('(/lang//' + @in_language + '/node())[1]', 'nvarchar(MAX)') AS bla
다음과 같이 값을 업데이트 할 수 있습니다.
UPDATE YOUR_TABLE
SET YOUR_XML_FIELD_NAME.modify('replace value of (/lang/de/text())[1] with ""I am a ''value ""')
WHERE id = 1
어디 대체 할 수 /lang/de/...
와 함께'.../' + @in_language + '/...'
PG hstore의 연관 배열에서 항목을 읽는 대신 XML 구문 분석의 오버 헤드로 인해 XML이 너무 느려져 XML 인코딩이 너무 어려워서 PostGre hstore와 같은 종류가 유용합니다.
방법 5 (SunWuKung에서 권장하는 방법 중 하나) : 각 "제품"테이블 당 하나의 변환 표. 이는 언어 당 하나의 행과 여러 개의 "텍스트"필드를 의미하므로 N 필드에는 하나의 (왼쪽) 조인 만 필요합니다. 그런 다음 "제품"테이블에 기본 필드를 쉽게 추가하고 변환 테이블을 쉽게 삭제하고 다시 삽입 할 수 있으며 사용자 정의 번역 (요청시)을위한 두 번째 테이블을 만들 수도 있습니다. 다시 삽입)하고 여전히 모든 외래 키가 있습니다.
이 작품을 볼 수있는 예를 만들어 봅시다.
먼저 테이블을 작성하십시오.
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
그런 다음 데이터를 입력하십시오
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
그런 다음 데이터를 쿼리하십시오.
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
게으른 경우 언어 테이블의 기본 키로 ISO-TwoLetterName ( 'DE', 'EN'등)을 사용할 수 있으므로 언어 ID를 조회 할 필요가 없습니다. 그러나 그렇게하는 경우 IETF 언어 태그를 대신 사용하고 싶을 것 입니다. 실제로 동일한 직교 법이 아닌 de-CH 및 de-DE를 얻으므로 더 좋습니다. 동일한 기본 언어이지만. 특히 en-US 및 en-GB / en-CA / en-AU 또는 fr-FR / fr-CA에 비슷한 문제가 있다는 점을 고려할 때 매우 중요합니다.
견적 : 우리는 그것을 필요로하지 않으며 소프트웨어는 영어로만합니다.
답 : 예-그러나 어느 것 ??
어쨌든 정수 ID를 사용하면 유연하고 나중에 메소드를 변경할 수 있습니다.
그리고 그 정수를 사용해야합니다. 왜냐하면 botched Db 디자인보다 더 성 가시고 파괴적이며 번거로운 것이 없기 때문입니다.
참조 RFC 5646 , ISO 639-2을 ,
그리고, 당신은 여전히 "우리가"말을하는 경우 에만 "오직 우리의 응용 프로그램을 하나 개 (보통 EN-US와 같은) 문화"- 그러므로 나는 여분의 정수, 이것이 좋은 시간과 언급하는 장소가 될 것이라고 필요가 없습니다 IANA 언어 태그 , 그렇지 않습니까?
그들은 다음과 같이 가기 때문에 :
de-DE-1901
de-DE-1996
과
de-CH-1901
de-CH-1996
(1996 년에 철자법 개혁이 있었다 ...) 철자가 틀린 단어를 사전에서 찾아보십시오. 이것은 법률 및 공공 서비스 포털을 다루는 응용 프로그램에서 매우 중요합니다.
더 중요한 것은 키릴 자모에서 라틴 알파벳으로 바뀌는 지역이 있으며, 일부 애매한 직교 법 개혁의 피상적 인 성가신 것보다 문제가 될 수 있기 때문에 거주 국가에 따라 중요한 고려 사항이 될 수 있습니다. 한 가지 방법 또는 다른 방법으로, 경우에 따라 정수를 갖는 것이 좋습니다.
편집 :
그리고 ON DELETE CASCADE
후에 추가하여
REFERENCES dbo.T_Products( PROD_Id )
간단히 말하면 DELETE FROM T_Products
외래 키 위반이 발생하지 않습니다.
데이터 정렬과 관련하여 다음과 같이합니다.
A) 고유 한 DAL 보유
B) 언어 테이블에 원하는 데이터 정렬 이름을 저장하십시오.
데이터 정렬을 자체 테이블에 넣을 수 있습니다. 예 :
SELECT * FROM sys.fn_helpcollations()
WHERE description LIKE '%insensitive%'
AND name LIKE '%german%'
C) 데이터 정렬 이름을 auth.user.language 정보에 제공하십시오
D) 다음과 같이 SQL을 작성하십시오.
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE {#COLLATION}
E) 그런 다음 DAL에서이 작업을 수행 할 수 있습니다.
cmd.CommandText = cmd.CommandText.Replace("{#COLLATION}", auth.user.language.collation)
그러면 완벽하게 구성된 SQL 쿼리가 제공됩니다.
SELECT
COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName
FROM T_Groups
ORDER BY GroupName COLLATE German_PhoneBook_CI_AI