多言語ソフトウェアを開発しています。アプリケーションコードに関する限り、ローカライズ可能性は問題ではありません。言語固有のリソースを使用し、それらに適したあらゆる種類のツールを使用できます。
しかし、多言語データベーススキーマを定義する最良の方法は何ですか?多数のテーブル(100以上)があり、各テーブルにローカライズ可能な複数の列を含めることができるとします(nvarchar列のほとんどはローカライズ可能です)。たとえば、テーブルの1つに製品情報が含まれている場合があります。
CREATE TABLE T_PRODUCT (
NAME NVARCHAR(50),
DESCRIPTION NTEXT,
PRICE NUMBER(18, 2)
)
NAME列とDESCRIPTION列で多言語テキストをサポートするための3つのアプローチを考えることができます。
言語ごとに個別の列
システムに新しい言語を追加する場合、次のように翻訳されたテキストを保存するために追加の列を作成する必要があります。
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)
)
各言語の列を含む翻訳テーブル
翻訳されたテキストを保存する代わりに、翻訳テーブルへの外部キーのみが保存されます。 translationsテーブルには、各言語の列が含まれています。
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
)
各言語の行を含む翻訳テーブル
翻訳されたテキストを保存する代わりに、翻訳テーブルへの外部キーのみが保存されます。 translationsテーブルにはキーのみが含まれ、個別のテーブルには言語への翻訳ごとの行が含まれます。
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)
)
各ソリューションには長所と短所がありますが、これらのアプローチの経験、推奨するもの、多言語データベーススキーマの設計方法について知りたいと思います。
翻訳可能な各テーブルに関連する変換テーブルがあるとどう思いますか?
CREATE TABLE T_PRODUCT(pr_id int、PRICE NUMBER(18、2))
CREATE TABLE T_PRODUCT_tr(pr_id INT FK、言語コードvarchar、pr_nameテキスト、pr_descrテキスト)
この方法では、複数の翻訳可能な列がある場合、それを取得するために単一の結合のみが必要になります。
これのマイナス面は、複雑な言語のフォールバックメカニズムがある場合、各変換テーブルにそれを実装する必要があるかもしれないということです-ストアドプロシージャに依存している場合。アプリからこれを行う場合、これはおそらく問題になりません。
あなたの考えをお聞かせください-私はまた、次のアプリケーションのためにこれについて決定をしようとしています。これまで、3番目のタイプを使用しました。
これは興味深い問題なので、壊死しましょう。
方法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>
がスキーマを変更し、古いテーブルが残されたが参照フィールドの名前が変更された場合、いかなる種類の警告も表示されません。レポートも機能しますwingパラメーターを選択せずに実行した場合(==> guid.empty)。しかし、突然、実際のユーザーが実際に翼を選択すると==> boomになります。 この方法は、あらゆる種類のテストを完全に中断します。
方法2:
簡単に言うと、「素晴らしい」アイデア(警告-皮肉)で、方法3の短所(多くのエントリがある場合の速度が遅い)と方法1のやや恐ろしい短所を組み合わせてみましょう。
この方法の唯一の利点は、すべての変換を1つのテーブルに保持するため、メンテナンスが簡単になることです。ただし、方法1と動的SQLストアドプロシージャ、翻訳を含む(一時的な)テーブル、およびターゲットテーブルの名前(およびすべてのテキストフィールドに同じ)。
方法3:
すべての翻訳に対して1つのテーブル:欠点:翻訳するn個のフィールドに対して、n個の外部キーを製品テーブルに保存する必要があります。したがって、nフィールドに対してn結合を行う必要があります。変換テーブルがグローバルである場合、多くのエントリがあり、結合が遅くなります。また、nフィールドに対してT_TRANSLATIONテーブルに常にn回参加する必要があります。これはかなりのオーバーヘッドです。さて、顧客ごとにカスタム翻訳に対応する必要がある場合はどうしますか?追加のテーブルに別の2x n結合を追加する必要があります。結合する必要がある場合は、10個のテーブルに2x2xn = 4nの追加結合を追加すると、なんて混乱するでしょう!また、この設計により、2つのテーブルで同じ変換を使用できます。あるテーブルのアイテム名を変更した場合、毎回別のテーブルのエントリも変更したいのですか?
さらに、製品テーブルに外部キーがあるため、テーブルを削除して再挿入することはできません...もちろん、FKの設定を省略することができ、<insert name of your "favourite" person here>
はテーブルを削除できます、およびnewid()ですべてのエントリを再挿入します[または挿入でidを指定しますが、identity-insert OFF]を使用します。すぐにデータガベージ(およびnull参照例外)につながります。
-- 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 + '/...'
に置き換えることができる場所
PostGre hstoreに似ていますが、(PG hstoreの連想配列からエントリを読み取る代わりに)XMLを解析するオーバーヘッドのために、非常に遅くなり、xmlエンコードが非常に苦痛になります。
このWORKSを見るための例を作りましょう:
まず、テーブルを作成します。
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-language tag を使用することをお勧めします。これは、de-CHとde-DEを取得するためです。どこでもßの代わりにdouble s)、それは同じベース言語ですが。特に、en-USとen-GB/en-CA/en-AUまたはfr-FR/fr-CAに同様の問題があることを考えると、あなたにとって重要なほんの少しの詳細です。
引用:私たちはそれを必要としません。私たちはソフトウェアを英語でしか行いません。
回答:はい-しかし、どれですか?
とにかく、整数IDを使用する場合、柔軟性があり、後でメソッドを変更できます。
。
RFC 5646 、 ISO 639-2 も参照してください
そして、まだ「we」onlyと言っている場合は、「only one culture」(通常en-USなど)のアプリケーションを作成します。したがって、余分な整数は必要ありません。これは IANA言語タグ に言及するのに良い時間と場所でしょう。
次のようになるためです。
de-DE-1901
de-DE-1996
そして
de-CH-1901
de-CH-1996
(1996年に正書法の改革がありました...)スペルが間違っている場合は、辞書でWordを見つけてみてください。これは、法的および公共サービスポータルを扱うアプリケーションで非常に重要になります。
さらに重要なことは、キリル文字からラテン文字に変化している地域があります。これは、あいまいな正書法改革の表面的な迷惑よりも厄介な場合があります。あなたが住んでいる国。いずれにせよ、万が一に備えて、そこに整数を入れた方が良いでしょう...
編集:
そして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
3番目のオプションは、いくつかの理由により最適です。
-アダム
この例を見てください:
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)
)
説明する必要はないと思います。構造はそれ自体を説明しています。
私は通常、このアプローチ(実際の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か所にまとめると、メンテナンスが非常に簡単になります。翻訳が翻訳局に外注される場合があります。これにより、1つの大きなエクスポートファイルを送信し、同じように簡単にインポートして戻すことができます。
ローカライズのヒントを探していたところ、このトピックを見つけました。私はこれがなぜ使用されるのか疑問に思っていました:
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'
技術的な詳細と解決策に進む前に、しばらく立ち止まって要件についていくつか質問してください。答えは技術的な解決策に大きな影響を与える可能性があります。そのような質問の例は次のとおりです。
-すべての言語が常に使用されますか?
-誰が、いつ、異なる言語バージョンで列を埋めますか?
-ユーザーがテキストの特定の言語を必要とし、システムに何もない場合はどうなりますか?
-テキストのみをローカライズするか、他のアイテムもあります(たとえば、価格は異なる可能性があるため、$と€に格納できます)
以下のアプローチは実行可能でしょうか?複数の列を翻訳する必要があるテーブルがあるとします。そのため、製品の場合、翻訳が必要な製品名と製品説明の両方を持つことができます。次のことができますか:
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)
)
ランダマイザーに同意します。テーブル「翻訳」が必要な理由がわかりません。
これで十分だと思います:
TA_product: ProductID, ProductPrice
TA_Language: LanguageID, Language
TA_Productname: ProductnameID, ProductID, LanguageID, ProductName
「どちらが最適か」は、プロジェクトの状況に基づいています。最初のものは選択と保守が簡単で、エンティティを選択するときにテーブルを結合する必要がないため、パフォーマンスも最適です。対象が2つまたは3つの言語のみをサポートし、それが増えないことを確認した場合は、それを使用できます。
2つ目はオーケーですが、理解して維持するのは困難です。そして、パフォーマンスは最初のものよりも悪いです。
最後の1つは拡張性は優れていますが、パフォーマンスは劣ります。 T_TRANSLATION_ENTRYテーブルはますます大きくなり、いくつかのテーブルからエンティティのリストを取得する場合はひどいものになります。