สคีมาสำหรับฐานข้อมูลหลายภาษา


235

ฉันกำลังพัฒนาซอฟต์แวร์หลายภาษา ตราบใดที่รหัสแอปพลิเคชันดำเนินไปการ localizability ไม่ใช่ปัญหา เราสามารถใช้ทรัพยากรเฉพาะภาษาและมีเครื่องมือทุกประเภทที่ทำงานได้ดีกับพวกเขา

แต่วิธีที่ดีที่สุดในการกำหนดสกีมาฐานข้อมูลหลายภาษาคืออะไร สมมติว่าเรามีตารางจำนวนมาก (100 หรือมากกว่า) และแต่ละตารางสามารถมีหลายคอลัมน์ที่สามารถแปลเป็นภาษาท้องถิ่นได้ (คอลัมน์ nvarchar ส่วนใหญ่ควรเป็นภาษาท้องถิ่น) ตัวอย่างเช่นหนึ่งในตารางอาจเก็บข้อมูลผลิตภัณฑ์:

CREATE TABLE T_PRODUCT (
  NAME        NVARCHAR(50),
  DESCRIPTION NTEXT,
  PRICE       NUMBER(18, 2)
)

ฉันสามารถนึกถึงสามวิธีในการสนับสนุนข้อความหลายภาษาในคอลัมน์ NAME และ DESCRIPTION:

  1. แยกคอลัมน์สำหรับแต่ละภาษา

    เมื่อเราเพิ่มภาษาใหม่ให้กับระบบเราต้องสร้างคอลัมน์เพิ่มเติมเพื่อเก็บข้อความที่แปลเช่นนี้:

    CREATE TABLE T_PRODUCT (
      NAME_EN        NVARCHAR(50),
      NAME_DE        NVARCHAR(50),
      NAME_SP        NVARCHAR(50),
      DESCRIPTION_EN NTEXT,
      DESCRIPTION_DE NTEXT,
      DESCRIPTION_SP NTEXT,
      PRICE          NUMBER(18,2)
    )
    
  2. ตารางการแปลพร้อมคอลัมน์สำหรับแต่ละภาษา

    แทนที่จะเก็บข้อความที่แปลแล้วจะมีเฉพาะคีย์ต่างประเทศไปยังตารางการแปลเท่านั้น ตารางการแปลมีคอลัมน์สำหรับแต่ละภาษา

    CREATE TABLE T_PRODUCT (
      NAME_FK        int,
      DESCRIPTION_FK int,
      PRICE          NUMBER(18, 2)
    )
    
    CREATE TABLE T_TRANSLATION (
      TRANSLATION_ID,
      TEXT_EN NTEXT,
      TEXT_DE NTEXT,
      TEXT_SP NTEXT
    )
    
  3. ตารางการแปลพร้อมแถวสำหรับแต่ละภาษา

    แทนที่จะเก็บข้อความที่แปลแล้วจะมีเฉพาะคีย์ต่างประเทศไปยังตารางการแปลเท่านั้น ตารางการแปลมีเพียงคีย์และตารางแยกต่างหากมีแถวสำหรับการแปลแต่ละภาษา

    CREATE TABLE T_PRODUCT (
      NAME_FK        int,
      DESCRIPTION_FK int,
      PRICE          NUMBER(18, 2)
    )
    
    CREATE TABLE T_TRANSLATION (
      TRANSLATION_ID
    )
    
    CREATE TABLE T_TRANSLATION_ENTRY (
      TRANSLATION_FK,
      LANGUAGE_FK,
      TRANSLATED_TEXT NTEXT
    )
    
    CREATE TABLE T_TRANSLATION_LANGUAGE (
      LANGUAGE_ID,
      LANGUAGE_CODE CHAR(2)
    )
    

มีข้อดีข้อเสียของแต่ละโซลูชันและฉันอยากรู้ว่าประสบการณ์ของคุณเกี่ยวกับวิธีการเหล่านี้คืออะไรคุณแนะนำอะไรและคุณจะออกแบบโครงร่างฐานข้อมูลหลายภาษาอย่างไร



3
คุณสามารถตรวจสอบลิงค์นี้: gsdesign.ro/blog/multilanguage-database-design-approachแม้ว่าการอ่านความคิดเห็นจะมีประโยชน์มาก
Fareed Alnamrouti

3
LANGUAGE_CODELANGUAGE_IDเป็นกุญแจสำคัญในธรรมชาติหลีกเลี่ยง
gavenkoa

1
ฉันเห็นแล้ว / ใช้ 2 และ 3 แล้วฉันไม่แนะนำคุณคุณสามารถจบด้วยแถวกำพร้า @ การออกแบบ SunWiKung นั้นดูดีกว่า IMO
Guillaume86

4
ฉันชอบการออกแบบของ SunWuKungs ซึ่งบังเอิญเป็นสิ่งที่เรานำมาใช้ อย่างไรก็ตามคุณต้องพิจารณาการเปรียบเทียบ อย่างน้อยใน SQL Server แต่ละคอลัมน์จะมีคุณสมบัติการเปรียบเทียบซึ่งกำหนดสิ่งต่าง ๆ เช่นการพิจารณาตัวพิมพ์เล็กและตัวพิมพ์ใหญ่ (หรือไม่) ของอักขระเน้นเสียงและการพิจารณาเฉพาะภาษาอื่น ๆ ไม่ว่าคุณจะใช้การจัดเรียงเฉพาะภาษาหรือไม่ขึ้นอยู่กับการออกแบบแอปพลิเคชันโดยรวมของคุณ แต่ถ้าคุณเข้าใจผิดมันจะยากที่จะเปลี่ยนในภายหลัง หากคุณต้องการการเปรียบเทียบภาษาเฉพาะคุณจะต้องมีคอลัมน์ต่อภาษาไม่ใช่แถวต่อภาษา
Elroy Flynn

คำตอบ:


113

คุณคิดอย่างไรเกี่ยวกับการมีตารางการแปลที่เกี่ยวข้องสำหรับแต่ละตารางที่แปลได้?

สร้างตาราง T_PRODUCT (pr_id int หมายเลขราคา (18, 2))

สร้างตาราง T_PRODUCT_tr (pr_id INT FK, languagecode varchar, ข้อความ pr_name, ข้อความ pr_descr)

วิธีนี้หากคุณมีคอลัมน์ที่แปลได้หลายคอลัมน์คุณจะต้องเข้าร่วมเพียงครั้งเดียวเพื่อรับ + ​​เนื่องจากคุณไม่ได้สร้างการแปลอัตโนมัติมันอาจจะง่ายกว่าที่จะนำเข้ารายการพร้อมกับการแปลที่เกี่ยวข้อง

ด้านลบของเรื่องนี้คือถ้าคุณมีกลไกทางเลือกภาษาที่ซับซ้อนคุณอาจจำเป็นต้องใช้มันสำหรับแต่ละตารางการแปล - หากคุณใช้กระบวนการที่เก็บไว้บางอย่าง หากคุณทำเช่นนั้นจากแอปสิ่งนี้อาจไม่เป็นปัญหา

แจ้งให้เราทราบว่าคุณคิดอย่างไร - ฉันกำลังตัดสินใจเกี่ยวกับเรื่องนี้สำหรับการสมัครครั้งต่อไปของเรา จนถึงตอนนี้เราได้ใช้ประเภทที่ 3 ของคุณแล้ว


2
ตัวเลือกนี้คล้ายกับตัวเลือกของฉัน nr 1 แต่ดีกว่า ยังคงรักษาได้ยากและต้องสร้างตารางใหม่สำหรับภาษาใหม่ดังนั้นฉันจึงลังเลที่จะใช้งาน
qbeuek

28
ไม่ต้องการตารางใหม่สำหรับภาษาใหม่ - คุณเพียงเพิ่มแถวใหม่ในตาราง _tr ที่เหมาะสมด้วยภาษาใหม่ของคุณคุณจะต้องสร้างตาราง _tr ใหม่หากคุณสร้างตารางที่แปลใหม่ได้

3
ฉันเชื่อว่านี่เป็นวิธีการที่ดี วิธีอื่นต้องใช้การรวมซ้ายจำนวนมากและเมื่อคุณเข้าร่วมหลายตารางที่แต่ละรายการมีการแปลอย่างลึก 3 ระดับและแต่ละรายการมี 3 ฟิลด์คุณต้องการ 3 * 3 9 เหลือเพียงการรวมสำหรับการแปลเท่านั้น .. ง่ายต่อการเพิ่มข้อ จำกัด ฯลฯ และฉันเชื่อว่าการค้นหานั้นเป็นกันเองมากขึ้น
GorillaApe

1
เมื่อT_PRODUCTมี 1 ล้านแถวT_PRODUCT_trจะมี 2 ล้านแถวมันจะลดประสิทธิภาพของ sql ได้มากแค่ไหน?
มิ ธ ริล

1
@Mithril ทั้งสองวิธีคุณมี 2 ล้านแถว อย่างน้อยคุณไม่จำเป็นต้องเข้าร่วมด้วยวิธีนี้
David D

56

นี่เป็นปัญหาที่น่าสนใจดังนั้นมาลองเรื่องลึกลับกันเถอะ

มาเริ่มจากปัญหาของวิธีที่ 1:
ปัญหา: คุณกำลังลดความเร็วในการบันทึกลง
ใน SQL (ยกเว้น PostGreSQL ด้วย hstore) คุณไม่สามารถผ่านภาษาพารามิเตอร์และพูดว่า:

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 (ซึ่งโปรแกรมเมอร์ทั่วไปที่มีความหลากหลายในสวนโดยทั่วไปจะไม่ทำและในกรณีของ รายงานผู้ใช้ว่าเป็นนรกจะไม่ทำเพื่อคุณแม้ว่าจะได้รับคำแนะนำให้ทำอย่างชัดเจน)
และ
ข) ความหมายมากที่สุดคุณหลวมชนิดของไวยากรณ์การตรวจสอบใดหาก<insert name of your "favourite" person here>เปลี่ยนแปลง schema เพราะทันใดความต้องการการเปลี่ยนปีกและสร้างตารางใหม่ตารางเก่าจะถูกทิ้ง แต่ฟิลด์การอ้างอิงเปลี่ยนชื่อคุณจะไม่ได้รับคำเตือนใด ๆ รายงานยังใช้งานได้เมื่อคุณรันโดยไม่เลือกพารามิเตอร์ wing (==> guid.empty) แต่ทันใดนั้นเมื่อผู้ใช้จริงเลือก wing ==>ความเจริญ วิธีนี้จะทำการทดสอบทุกรูปแบบอย่างสมบูรณ์


วิธีที่ 2:
สรุป: ความคิด "ยอดเยี่ยม" (คำเตือน - การประชดประชัน) เรามารวมข้อเสียของวิธีที่ 3 (ความเร็วช้าเมื่อมีหลายรายการ) เข้ากับข้อเสียที่ค่อนข้างน่ากลัวของวิธีที่ 1
ข้อดีเพียงอย่างเดียวของวิธีนี้คือ การแปลทั้งหมดในตารางเดียวและทำให้บำรุงรักษาง่าย อย่างไรก็ตามสิ่งเดียวกันสามารถเกิดขึ้นได้ด้วยวิธีที่ 1 และขั้นตอนการจัดเก็บ SQL แบบไดนามิกและตาราง (อาจชั่วคราว) ที่มีการแปลและชื่อของตารางเป้าหมาย (และค่อนข้างง่ายสมมติว่าคุณตั้งชื่อฟิลด์ข้อความทั้งหมดของคุณ เหมือนกัน).


วิธีที่ 3:
ตารางหนึ่งสำหรับการแปลทั้งหมด: ข้อเสีย: คุณต้องเก็บ n Foreign Keys ในตารางผลิตภัณฑ์สำหรับ n field ที่คุณต้องการแปล ดังนั้นคุณต้องทำ n รวมสำหรับฟิลด์ n เมื่อตารางการแปลเป็นสากลมันมีหลายรายการและเข้าร่วมช้า นอกจากนี้คุณต้องเข้าร่วม T_TRANSLATION ตาราง n ครั้งสำหรับฟิลด์ n นี่เป็นค่าโสหุ้ย ตอนนี้คุณจะทำอย่างไรเมื่อคุณต้องรองรับการแปลที่กำหนดเองต่อลูกค้า? คุณจะต้องเพิ่ม 2xn รวมเข้ากับตารางเพิ่มเติม ถ้าคุณต้องเข้าร่วมพูดถึง 10 โต๊ะโดยมีการเข้าร่วมเพิ่มเติม 2x2xn = 4n มันช่างเป็นระเบียบ! นอกจากนี้การออกแบบนี้ทำให้สามารถใช้การแปลเดียวกันกับ 2 ตาราง ถ้าฉันเปลี่ยนชื่อรายการในหนึ่งตารางฉันต้องการเปลี่ยนรายการในตารางอื่นด้วยเช่นกันทุกครั้งหรือไม่

ยิ่งไปกว่านั้นคุณไม่สามารถลบและแทรกตารางอีกต่อไปได้เนื่องจากขณะนี้มีคีย์ต่างประเทศในตารางผลิตภัณฑ์ ... คุณสามารถละเว้นการตั้งค่า FK จากนั้น<insert name of your "favourite" person here>สามารถลบตารางและแทรกใหม่ได้ รายการทั้งหมดที่มีnewid () [หรือโดยการระบุรหัสในส่วนแทรก แต่มีการปิดการใส่ข้อมูลประจำตัว ] และสิ่งนั้นจะนำไปสู่ข้อมูลขยะ (และข้อยกเว้นอ้างอิงโมฆะ) ในไม่ช้า


วิธีที่ 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&amp;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 

จากนั้นคุณสามารถรับค่าโดย XPath-Query ใน SQL ซึ่งคุณสามารถใส่ตัวแปรสตริงได้

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 "&quot;I am a ''value &quot;"')
WHERE id = 1 

ที่ซึ่งคุณสามารถแทนที่/lang/de/...ด้วย'.../' + @in_language + '/...'

ชนิดของ PostGre hstore ยกเว้นว่าเนื่องจากโอเวอร์เฮดของการแยกวิเคราะห์ XML (แทนที่จะอ่านรายการจากอาเรย์แบบเชื่อมโยงใน PG hstore) มันช้าเกินไปและการเข้ารหัส xml ทำให้มันเจ็บปวดเกินกว่าจะเป็นประโยชน์


วิธีที่ 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' ฯลฯ ) เป็นคีย์หลักของตารางภาษาได้ดังนั้นคุณไม่จำเป็นต้องค้นหารหัสภาษา แต่ถ้าคุณทำเช่นนั้นคุณอาจต้องการใช้แท็กภาษา IETFแทนซึ่งดีกว่าเพราะคุณได้รับ de-CH และ de-DE ซึ่งจริงๆแล้วไม่ใช่ ortography-wise เดียวกัน (double s แทนßทุกที่) แม้ว่ามันจะเป็นภาษาฐานเดียวกัน นั่นเป็นเพียงรายละเอียดเล็ก ๆ น้อย ๆ ที่อาจสำคัญกับคุณโดยเฉพาะเมื่อพิจารณาว่า en-US และ en-GB / en-CA / en-AU หรือ fr-FR / fr-CA มีปัญหาที่คล้ายกัน
อ้างอิง: เราไม่ต้องการเราทำแค่ซอฟต์แวร์เป็นภาษาอังกฤษเท่านั้น
คำตอบ: ใช่ - แต่อันไหน ??

อย่างไรก็ตามหากคุณใช้ ID จำนวนเต็มคุณจะมีความยืดหยุ่นและสามารถเปลี่ยนวิธีการของคุณได้ในภายหลัง
และคุณควรใช้จำนวนเต็มนั้นเพราะไม่มีอะไรน่ารำคาญทำลายและลำบากไปกว่าการออกแบบ 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-Query อย่างสมบูรณ์

SELECT 
    COALESCE(GRP_Name_i18n_cust, GRP_Name_i18n, GRP_Name) AS GroupName 
FROM T_Groups 

ORDER BY GroupName COLLATE German_PhoneBook_CI_AI

ตอบสนองรายละเอียดดีขอบคุณมาก แต่คุณคิดอย่างไรเกี่ยวกับปัญหาการเรียงในโซลูชัน 5 วิธี ดูเหมือนว่านี่ไม่ใช่วิธีที่ดีที่สุดเมื่อคุณต้องการเรียงลำดับหรือกรองข้อความที่แปลแล้วในสภาพแวดล้อมหลายภาษาด้วยการเปรียบเทียบที่แตกต่างกัน และในกรณีเช่นนี้วิธีที่ 2 (ซึ่งคุณ "ทำให้แย่ลง" อย่างรวดเร็ว :)) อาจเป็นตัวเลือกที่ดีกว่าด้วยการแก้ไขเล็กน้อยซึ่งบ่งชี้การเปรียบเทียบเป้าหมายสำหรับแต่ละคอลัมน์ที่แปลแล้ว
Eugene Evdokimov

2
@Eugene Evdokimov: ใช่ แต่ "ORDER BY" จะเป็นปัญหาเสมอเพราะคุณไม่สามารถระบุได้ว่าเป็นตัวแปร แนวทางของฉันคือการบันทึกชื่อการเรียงในตารางภาษาและมีสิ่งนี้ใน userinfo จากนั้นในแต่ละคำสั่ง SQL คุณสามารถพูด ORDER BY COLUMN_NAME {#collation} และคุณสามารถแทนที่ใน dal ของคุณ (cmd.CommandText = cmd.CommandText.Replace ("{# COLLATION}", auth.user language.collation) อีกวิธีหนึ่งคุณสามารถเรียงลำดับในรหัสแอปพลิเคชันของคุณเช่นใช้ LINQ ซึ่งจะใช้เวลาประมวลผลโหลดฐานข้อมูลของคุณสำหรับรายงานรายงานจะเรียงลำดับต่อไป
Stefan Steiger

oo นี่ต้องเป็นคำตอบ SO ที่ยาวที่สุดที่ฉันเคยเห็นและฉันเห็นคนทำรายการทั้งหมดในคำตอบ คุณเป็นคนดี
Domino

สามารถยอมรับโซลูชั่นของ SunWuKung ได้ดีที่สุด
Domi

48

ตัวเลือกที่สามนั้นดีที่สุดด้วยเหตุผลบางประการ:

  • ไม่ต้องการแก้ไขสกีมาฐานข้อมูลสำหรับภาษาใหม่ (และ จำกัด การเปลี่ยนแปลงรหัส)
  • ไม่ต้องใช้พื้นที่มากสำหรับภาษาที่ไม่ได้นำไปใช้งานหรือการแปลของรายการเฉพาะ
  • ให้ความยืดหยุ่นสูงสุด
  • คุณไม่ได้ลงเอยด้วยตารางหร็อมแหร็ม
  • คุณไม่ต้องกังวลเกี่ยวกับปุ่ม null และตรวจสอบว่าคุณกำลังแสดงการแปลที่มีอยู่แทนรายการ null บางอย่าง
  • หากคุณเปลี่ยนหรือขยายฐานข้อมูลของคุณเพื่อรวมรายการ / สิ่ง / etc ที่สามารถแปลได้อื่น ๆ คุณสามารถใช้ตารางและระบบเดียวกันได้ซึ่งไม่ได้แยกจากส่วนที่เหลือของข้อมูล

อดัม


1
ฉันเห็นด้วยแม้ว่าโดยส่วนตัวแล้วฉันจะมีตารางที่แปลเป็นภาษาท้องถิ่นสำหรับแต่ละตารางหลักเพื่ออนุญาตให้ใช้คีย์ต่างประเทศได้
Neil Barnwell

1
แม้ว่าตัวเลือกที่สามคือการใช้งานที่สะอาดและถูกต้องที่สุดของปัญหา แต่ก็มีความซับซ้อนมากกว่าตัวเลือกแรก ฉันคิดว่าการแสดงการแก้ไขการรายงานเวอร์ชันทั่วไปต้องใช้ความพยายามเป็นพิเศษมากจนไม่สามารถยอมรับได้เสมอไป ฉันได้ติดตั้งทั้งสองวิธีแล้วสิ่งที่ง่ายกว่าก็เพียงพอแล้วเมื่อผู้ใช้ต้องการการแปลภาษาแอปพลิเคชัน "หลัก" แบบอ่านอย่างเดียว (บางครั้งหายไป)
rics

12
จะเกิดอะไรขึ้นถ้าตารางผลิตภัณฑ์มีหลายฟิลด์ที่แปล? เมื่อดึงผลิตภัณฑ์คุณจะต้องเข้าร่วมเพิ่มเติมหนึ่งครั้งต่อหนึ่งฟิลด์ที่แปลแล้วซึ่งจะส่งผลให้เกิดปัญหาประสิทธิภาพการทำงานที่รุนแรง มีความซับซ้อนเพิ่มเติม (IMO) เพิ่มเติมสำหรับการแทรก / อัปเดต / ลบ ข้อได้เปรียบเดียวของสิ่งนี้คือจำนวนตารางที่ต่ำกว่า ฉันจะใช้วิธีการที่เสนอโดย SunWuKung: ฉันคิดว่ามันเป็นความสมดุลที่ดีระหว่างปัญหาความซับซ้อนและปัญหาการบำรุงรักษา
Frosty Z

@ rics- ฉันเห็นด้วยคุณแนะนำให้ ... อะไร
ดาบ

@ Adam- ฉันสับสนบางทีฉันอาจเข้าใจผิด คุณแนะนำอันที่สามใช่มั้ย โปรดอธิบายในรายละเอียดเพิ่มเติมความสัมพันธ์ระหว่างตารางเหล่านั้นจะเป็นอย่างไร คุณหมายถึงเราต้องใช้การแปลและ TranslationEntry ตารางสำหรับแต่ละตารางใน DB?
ดาบ

9

ลองดูตัวอย่างนี้:

PRODUCTS (
    id   
    price
    created_at
)

LANGUAGES (
    id   
    title
)

TRANSLATIONS (
    id           (// id of translation, UNIQUE)
    language_id  (// id of desired language)
    table_name   (// any table, in this case PRODUCTS)
    item_id      (// id of item in PRODUCTS)
    field_name   (// fields to be translated)
    translation  (// translation text goes here)
)

ฉันคิดว่าไม่จำเป็นต้องอธิบายโครงสร้างอธิบายตัวเอง


ดีจัง. แต่คุณจะค้นหาได้อย่างไร (เช่น product_name)
Illuminati

คุณมีตัวอย่างสดจากตัวอย่างของคุณบ้างไหม? คุณได้รับปัญหาใด ๆ จากการใช้หรือไม่
David Létourneau

แน่นอนว่าฉันมีโครงการอสังหาริมทรัพย์หลายภาษาเรารองรับ 4 ภาษา การค้นหาค่อนข้างซับซ้อน แต่ก็รวดเร็ว แน่นอนในโครงการขนาดใหญ่อาจช้ากว่าที่ควรจะเป็น ในโครงการขนาดเล็กหรือขนาดกลางก็โอเค
bamburik

8

ฉันมักจะไปสำหรับวิธีนี้ (ไม่ใช่ sql จริง) นี้สอดคล้องกับตัวเลือกสุดท้ายของคุณ

table Product
productid INT PK, price DECIMAL, translationid INT FK

table Translation
translationid INT PK

table TranslationItem
translationitemid INT PK, translationid INT FK, text VARCHAR, languagecode CHAR(2)

view ProductView
select * from Product
inner join Translation
inner join TranslationItem
where languagecode='en'

เพราะการมีข้อความที่แปลได้ทั้งหมดในที่เดียวทำให้การบำรุงรักษาง่ายขึ้นมาก บางครั้งการแปลจะถูกส่งไปที่สำนักงานการแปลด้วยวิธีนี้คุณสามารถส่งไฟล์ส่งออกขนาดใหญ่เพียงไฟล์เดียวและนำเข้ากลับได้อย่างง่ายดาย


1
TranslationตารางหรือTranslationItem.translationitemidคอลัมน์มีจุดประสงค์อะไร
DanMan

4

ก่อนไปที่รายละเอียดทางเทคนิคและวิธีแก้ไขคุณควรหยุดสักครู่แล้วถามคำถามสองสามข้อเกี่ยวกับข้อกำหนด คำตอบอาจมีผลกระทบอย่างมากต่อการแก้ปัญหาทางเทคนิค ตัวอย่างของคำถามดังกล่าวจะเป็น:
- จะใช้ภาษาทั้งหมดตลอดเวลาหรือไม่
- ใครและเมื่อใดที่จะเติมคอลัมน์ด้วยภาษาที่แตกต่างกัน?
- จะเกิดอะไรขึ้นเมื่อผู้ใช้ต้องการภาษาที่แน่นอนของข้อความและไม่มีในระบบ?
- เฉพาะข้อความที่จะแปลเป็นภาษาท้องถิ่นหรือมีรายการอื่น ๆ (เช่นราคาสามารถเก็บไว้ใน $ และ€เพราะอาจแตกต่างกัน)


ฉันรู้ว่าการโลคัลไลซ์เซชั่นเป็นหัวข้อที่กว้างกว่ามากและฉันทราบถึงปัญหาที่คุณแจ้งมา แต่ตอนนี้ฉันกำลังมองหาคำตอบสำหรับปัญหาเฉพาะของการออกแบบสคีมา ฉันคิดว่าภาษาใหม่จะถูกเพิ่มทีละส่วนและแต่ละภาษาจะถูกแปลเกือบทั้งหมด
qbeuek

3

ฉันกำลังมองหาเคล็ดลับสำหรับการแปลและพบหัวข้อนี้ ฉันสงสัยว่าทำไมจึงใช้สิ่งนี้:

CREATE TABLE T_TRANSLATION (
   TRANSLATION_ID
)

ดังนั้นคุณจะได้รับสิ่งที่ต้องการ user39603 แนะนำ:

table Product
productid INT PK, price DECIMAL, translationid INT FK

table Translation
translationid INT PK

table TranslationItem
translationitemid INT PK, translationid INT FK, text VARCHAR, languagecode CHAR(2)

view ProductView
select * from Product
inner join Translation
inner join TranslationItem
where languagecode='en'

คุณไม่สามารถออกจากโต๊ะการแปลเพื่อที่คุณจะได้:

    table Product
    productid INT PK, price DECIMAL

    table ProductItem
    productitemid INT PK, productid INT FK, text VARCHAR, languagecode CHAR(2)

    view ProductView
    select * from Product
    inner join ProductItem
    where languagecode='en'

1
แน่ใจ ฉันจะโทรหาProductItemโต๊ะที่ชอบProductTextsหรือProductL10nว่า ทำให้รู้สึกมากขึ้น
DanMan

1

ฉันเห็นด้วยกับ randomizer ฉันไม่เห็นสาเหตุที่คุณต้องการตาราง "แปล"

ฉันคิดว่านี่พอ:

TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName

1

วิธีการด้านล่างนี้จะเป็นไปได้หรือไม่ สมมติว่าคุณมีตารางที่มีมากกว่า 1 คอลัมน์ที่ต้องการการแปล ดังนั้นสำหรับผลิตภัณฑ์คุณสามารถมีทั้งชื่อผลิตภัณฑ์และคำอธิบายผลิตภัณฑ์ที่ต้องการการแปล คุณสามารถทำสิ่งต่อไปนี้:

CREATE TABLE translation_entry (
      translation_id        int,
      language_id           int,
      table_name            nvarchar(200),
      table_column_name     nvarchar(200),
      table_row_id          bigint,
      translated_text       ntext
    )

    CREATE TABLE translation_language (
      id int,
      language_code CHAR(2)
    )   

0

"อันไหนดีที่สุด" ขึ้นอยู่กับสถานการณ์ของโครงการ คนแรกนั้นง่ายต่อการเลือกและบำรุงรักษาและประสิทธิภาพที่ดีที่สุดเพราะมันไม่จำเป็นต้องเข้าร่วมตารางเมื่อเลือกเอนทิตี หากคุณยืนยันว่า Poject ของคุณรองรับได้เพียง 2 หรือ 3 ภาษาเท่านั้นและจะไม่เพิ่มขึ้นคุณสามารถใช้มันได้

อย่างที่สองก็โอเค แต่ยากที่จะเข้าใจและบำรุงรักษา และประสิทธิภาพนั้นแย่กว่าครั้งแรก

อันสุดท้ายนั้นดีในเรื่องความสามารถในการปรับขยายได้ แต่ประสิทธิภาพในการทำงานแย่ ตาราง T_TRANSLATION_ENTRY จะมีขนาดใหญ่ขึ้นเรื่อย ๆ เป็นเรื่องที่น่ากลัวเมื่อคุณต้องการดึงข้อมูลรายการเอนทิตีจากบางตาราง


0

เอกสารนี้อธิบายถึงแนวทางแก้ไขที่เป็นไปได้และข้อดีและข้อเสียของแต่ละวิธี ฉันชอบ "การแปลแถว" เพราะคุณไม่จำเป็นต้องแก้ไข DB schema เมื่อเพิ่มภาษาใหม่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.