2012-11-17 9 views
15

私は国際化への簡単なアプローチのJPA実装をしようとしています。私は複数のテーブルの複数のフィールドで参照できる翻訳された文字列のテーブルを持っています。したがって、すべてのテーブル内のすべてのテキストの出現は、翻訳された文字列テーブルへの参照に置き換えられます。言語IDと組み合わせると、翻訳された文字列テーブルにその特定のフィールドの一意の行が与えられます。JPA国際化のためのデータベース構造

コース int型Course_IDに、 int型の名前、 int型の記述

モジュール int型MODULE_ID、 int型の名前

- :たとえば、次のように実体のコースとモジュールを持つスキーマを検討

course.name、course.descriptionおよびmodule.nameはすべて、翻訳された文字列テーブルのidフィールドを参照しています。 -

int型のID、すべては十分に簡単なようだ 文字列LANG、 文字列の内容

をTranslatedString。国際化できるすべての文字列に対して1つのテーブルを取得し、そのテーブルは他のすべてのテーブルで使用されます。

eclipselink 2.4を使用してJPAでこれを行う方法はありますか?

私は埋め込まれたElementCollectionを見てきました。これは... JPA 2.0: Mapping a Map - 翻訳された文字列テーブルを所有テーブルのpkに関連付けるように見えます。これは、エンティティごとに1つの翻訳可能な文字列フィールドしか持つことができないことを意味します(変換可能な文字列テーブルに新しい結合カラムを追加する場合を除き、ポイントを破棄します)。また、エンティティ間でどのように動作するかについても明確ではありません。おそらく、各エンティティのIDは、変換可能な文字列テーブルの一意性を保証するためにデータベース全体のシーケンスを使用する必要があります。

私は、そのリンクに配置された例を試してみましたが、それは私のためには機能しませんでした - ローカル化されたStringマップが追加されるとすぐにクライアント側が爆弾を発生させました。サーバサイドで何もDBに残っていない:S

これまでの約9時間のうちに私は家にいましたが、私はこのInternationalization with Hibernateを見ましたが、上記のリンクと同じことをしようとしているようですテーブルは彼が達成したものを見るのが難しいと定義しています)。この時点でどんな助けも喜んで達成されるでしょう...

編集1 - 以下のAMSアンサーを編集して、実際に問題を解決するかどうかはわかりません。彼の例では、他のプロセスに説明テキストを保存したままにしています。このタイプのアプローチのアイデアは、エンティティオブジェクトがテキストとロケールを取り、これ(何とか!)が翻訳可能な文字列テーブルに終わるということです。私が与えた最初のリンクでは、男は組み込み地図を使ってこれをやろうとしていますが、これは正しいアプローチだと思います。彼の方法には2つの問題があります - 一つはうまくいかないようです! 2つはうまくいけば、別の方法ではなく埋め込みテーブルにFKを格納しています(私はそれを実行することができないので、それがどのように持続するか正確に見ることはできません)。私は正しいアプローチは、翻訳が必要な各テキスト(mapはlocale-> contentである)の代わりにマップ参照で終了すると思われますが、1つのエンティティで複数のマップを許可する方法でこれを行う方法がわかりません(翻訳可能な文字列テーブルに対応する複数の列を持たない)...

答えて

6

OK、私はそれを持っていると思います。私の質問の最初のリンクの単純化されたバージョンが動作し、Localizedエンティティ(メインエンティティの各テキスト要素ごとに異なるjoinColumnを持つ)にManyToOne関係を使用するだけで、そのローカライズされたエンティティ内のMapの単純なElementCollection 。私は、各ロケール(名前と説明)ごとに複数のエントリが必要な2つのテキスト要素を持つ、ただ1つのエンティティ(カテゴリ)で、私の質問とは若干異なる例をコーディングしました。

これは、MySQLに行くEclipselink 2.4に対して行われたことに注意してください。

最初のリンクからわかるように、ElementCollectionを使用すると、別のテーブルが作成され、変換可能な文字列の2つのテーブルが作成されます.1つはID(Locaised)すべての地図情報を保持するメインのFK(Localised_strings)。 Localised_stringsという名前は自動/デフォルトの名前です。@CollectionTableアノテーションで別の名前を使用できます。全体として、これはDBの観点からは理想的ではありませんが、世界の終わりではありません。

2番目は、少なくともEclipselinkとMySQLの組み合わせで、単一の(自動生成された)列テーブルに永続化するとエラーが発生します:(したがって、エンティティのダミー列の既定値を追加しました。純粋にその問題を克服することである

import java.io.Serializable; 
import java.lang.Long; 
import java.lang.String; 
import java.util.HashMap; 
import java.util.Map; 

import javax.persistence.*; 


@Entity 

public class Category implements Serializable { 

@GeneratedValue(strategy = GenerationType.IDENTITY) 
@Id 
private Long id; 
@ManyToOne(cascade=CascadeType.ALL) 
@JoinColumn(name="NAME_ID") 
private Localised nameStrings = new Localised(); 

@ManyToOne(cascade=CascadeType.ALL) 
@JoinColumn(name="DESCRIPTION_ID") 
private Localised descriptionStrings = new Localised(); 

private static final long serialVersionUID = 1L; 

public Category() { 

    super(); 
} 

public Category(String locale, String name, String description){ 
    this.nameStrings.addString(locale, name); 
    this.descriptionStrings.addString(locale, description); 
} 
public Long getId() { 
    return this.id; 
} 

public void setId(Long id) { 
    this.id = id; 
} 

public String getName(String locale) { 
    return this.nameStrings.getString(locale); 
} 

public void setName(String locale, String name) { 
    this.nameStrings.addString(locale, name); 
} 
public String getDescription(String locale) { 
    return this.descriptionStrings.getString(locale); 
} 

public void setDescription(String locale, String description) { 
    this.descriptionStrings.addString(locale, description); 
} 

} 




import java.util.HashMap; 
import java.util.Map; 

import javax.persistence.ElementCollection; 
import javax.persistence.Embeddable; 
import javax.persistence.Entity; 
import javax.persistence.GeneratedValue; 
import javax.persistence.GenerationType; 
import javax.persistence.Id; 

@Entity 
public class Localised { 

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY) 
    private int id; 
    private int dummy = 0; 
    @ElementCollection 
    private Map<String,String> strings = new HashMap<String, String>(); 

    //private String locale;  
    //private String text; 

    public Localised() {} 

    public Localised(Map<String, String> map) { 
     this.strings = map; 
    } 

    public void addString(String locale, String text) { 
     strings.put(locale, text); 
    } 

    public String getString(String locale) { 
     String returnValue = strings.get(locale); 
     return (returnValue != null ? returnValue : null); 
    } 

} 

次のようにこれらのテーブルを生成します - 。

CREATE TABLE LOCALISED (ID INTEGER AUTO_INCREMENT NOT NULL, DUMMY INTEGER, PRIMARY KEY (ID)) 
CREATE TABLE CATEGORY (ID BIGINT AUTO_INCREMENT NOT NULL, DESCRIPTION_ID INTEGER, NAME_ID INTEGER, PRIMARY KEY (ID)) 
CREATE TABLE Localised_STRINGS (Localised_ID INTEGER, STRINGS VARCHAR(255), STRINGS_KEY VARCHAR(255)) 
ALTER TABLE CATEGORY ADD CONSTRAINT FK_CATEGORY_DESCRIPTION_ID FOREIGN KEY (DESCRIPTION_ID) REFERENCES LOCALISED (ID) 
ALTER TABLE CATEGORY ADD CONSTRAINT FK_CATEGORY_NAME_ID FOREIGN KEY (NAME_ID) REFERENCES LOCALISED (ID) 
ALTER TABLE Localised_STRINGS ADD CONSTRAINT FK_Localised_STRINGS_Localised_ID FOREIGN KEY (Localised_ID) REFERENCES LOCALISED (ID) 

Aメイン...

import java.util.List; 

import javax.persistence.EntityManager; 
import javax.persistence.EntityManagerFactory; 
import javax.persistence.Persistence; 
import javax.persistence.Query; 

public class Main { 
    static EntityManagerFactory emf = Persistence.createEntityManagerFactory("javaNetPU"); 
    static EntityManager em = emf.createEntityManager(); 

    public static void main(String[] a) throws Exception { 
    em.getTransaction().begin(); 


    Category category = new Category(); 

    em.persist(category); 

    category.setName("EN", "Business"); 
    category.setDescription("EN", "This is the business category"); 


    category.setName("FR", "La Business"); 
    category.setDescription("FR", "Ici es la Business"); 

    em.flush(); 

    System.out.println(category.getDescription("EN")); 
    System.out.println(category.getName("FR")); 


    Category c2 = new Category(); 
    em.persist(c2); 

    c2.setDescription("EN", "Second Description"); 
    c2.setName("EN", "Second Name"); 

    c2.setDescription("DE", "Zwei Description"); 
    c2.setName("DE", "Zwei Name"); 

    em.flush(); 


    //em.remove(category); 


    em.getTransaction().commit(); 
    em.close(); 
    emf.close(); 

    } 
} 
それをテストします

これは、出力生成: -

This is the business category 
La Business 

と、次の表のエントリを: - em.removeのコメントを外す

Category 
"ID" "DESCRIPTION_ID" "NAME_ID" 
"1"   "1"     "2" 
"2"   "3"     "4" 

Localised 
"ID" "DUMMY" 
"1"   "0" 
"2"   "0" 
"3"   "0" 
"4"   "0" 

Localised_strings 

"Localised_ID" "STRINGS"      "STRINGS_KEY" 
"1"     "Ici es la Business"     "FR" 
"1"     "This is the business category"  "EN" 
"2"     "La Business"      "FR" 
"2"     "Business"      "EN" 
"3"     "Second Description"     "EN" 
"3"     "Zwei Description"    "DE" 
"4"     "Second Name"      "EN" 
"4"     "Zwei Name"       "DE" 

は正しくカテゴリーの両方を削除し、それがLocaised/Localised_stringsエントリを関連付けられています。

誰もが将来誰かを助けることを願っています。

+0

これは素晴らしいです! JPA2.0 + Hibernate 4.2 + MSSQL2008でうまく動作します。パフォーマンスなどの可能性のある問題について考えましたか?私は熱心にロードされたローカライズされた文字列を取得したいと思います。ところで、ローカライズされたテーブルのダミー列は必要ありません。 – curious1

+0

上記のように、ダミー列の問題はEclipselinkとMySQLの特定の組み合わせにまで及んでいると思います.HibernateとMSQLのコンボのように見えますが、単一の自動列が保持されます。 私が探していた特定のアプリケーションのボリューム要件は小さかったので、パフォーマンス上の問題に多大な時間を費やすことはありませんでした。 – IrishDubGuy

+0

spring data restアプリケーションのaccept-languageヘッダーの値に対応する文字列のみを取得するのが最善の方法ですか? – aycanadal

-1

これを行う方法が1つあります。

データベースから翻訳されたすべての文字列をキャッシュにロードすると、それはMessagesCacheと呼ばれ、public String getMesssage(int id、int languageCode)というメソッドがあります。これをメモリキャッシュに保存するには、google guava不変のコレクションを使用できます。オンデマンドでロードする場合は、Guava LoadingCacheを使用してキャッシュを保存することもできます。このようなキャッシュがあれば、このようなコードを書くことができます。

@Entity 
public Course { 
    @Column("description_id") 
    private int description; 

    public String getDescription(int languageCode) 
    { 
     return this.messagesCache(description, languageCode); 
    } 


    public String setDscription(int descriptionId) 
    { 
     this.description = descriptionId; 
    } 
} 

私はこのアプローチで見る主な問題は、あなたがエンティティで参照されているロケールを知る必要があることを、私は説明のために正しい言語を選ぶのタスクはしていない行うべきであることを示唆しているのですDaoやServiceのような高レベルの抽象化であるエンティティ。

+0

これは本当に問題には触れていませんが、テキストがどのように保存されるかなどは無視されます。上記の私の編集を参照してください。 – IrishDubGuy

+0

セッターで文字列を返すことはできません。 JPAのセッターとゲッターは、同じ値をポストして返すべきです。 –

9

(私はhwellmanのブログに返信しました。)私の初期のアプローチはあなたのアプローチと非常によく似ており、仕事をしています。これは、他のより具体的なテーブルを参照する必要のない一般的なデータベーステーブルを使用して、任意のエンティティの任意のフィールドがローカライズされたString Mapを参照できるという要件を満たします。実際、私はProductエンティティの複数のフィールド(名前、説明、詳細)にも使用します。 JPAが主キー列とこのIDを参照する値の表のみを持つ表を生成したという「問題」もありました。OpenJPAでは、ダミー列は必要ありませんでした。

public class StringI18N { 

    @OneToMany(mappedBy = "parent", cascade = ALL, fetch = EAGER, orphanRemoval = true) 
    @MapKey(name = "locale") 
    private Map<Locale, StringI18NSingleValue> strings = new HashMap<Locale, StringI18NSingleValue(); 
... 

OpenJPAは単にLocaleをStringとして格納します。私たちは本当に余分なエンティティStringI18NSingleValueを必要としないので、@ ElementCollectionを使ったマッピングはもう少しエレガントです。

複数のエンティティでローカライズされたものを共有することは許可されていますか?オーナーエンティティが削除されたときに孤立したローカライズされたエンティティをどのようにして防ぐのですか?単純にカスケードを使用するだけでは十分ではありません。私はLocalizedを可能な限り「価値オブジェクト」として見て、それを他のエンティティと共有することを許可しないことで、同じLocalizedへの複数の参照について考える必要がなくなり、孤児の削除を安全に使用できるようになりました。私のユースケースに応じて、私はまた、偽または真= EAGER/LAZYとオプションを=フェッチ使用

@OneToOne(cascade = ALL, orphanRemoval = true) 

:だから私のローカライズされたフィールドは次のようにマッピングされます。 optional = falseを使用する場合、@JoinColumn(nullable = false)を使用するため、OpenJPAは結合列にNOT NULL制約を生成します。

ローカライズされたものを別のエンティティにコピーする必要があるときはいつでも、同じ参照は使用しませんが、同じ内容とIDのない新しいローカライズされたインスタンスを作成します。それ以外の場合は、changinを実行しないと、複数のエンティティとインスタンスを共有していて、ローカライズされた文字列を変更すると、別のエンティティで別の文字列を変更する可能性があります。

これまでのところ、実際には、OpenJPAには、1つ以上のローカライズされた文字列を含むエンティティを選択するときにN + 1選択問題があることがわかりました。それは効率的に要素コレクションを取得しません(私はこれをhttps://issues.apache.org/jira/browse/OPENJPA-1920と報告しました)。この問題は恐らくMap < Locale、StringI18NSingleValue >を使用して解決されます。しかし、OpenJPAは、A.1.1 B 1 .. * Cという構造の構造を効率的に取得することもできません。これは、ここで何が起こるかです(私はこれをhttps://issues.apache.org/jira/browse/OPENJPA-2296として報告しました)。これはアプリケーションのパフォーマンスに重大な影響を与えます。

他のJPAプロバイダにも同様のN + 1選択問題がある可能性があります。カテゴリを取得するパフォーマンスが懸念される場合は、カテゴリを取得するために使用されるクエリの数がエンティティの数に依存するかどうかを確認します。私は、Hibernateではバッチフェッチやサブセレクトを強制してこれらの問題を解決できることを知っています。また、EclipseLinkにも同様の機能があり、動作しない場合があります。

このパフォーマンスの問題を解決するには、私は本当に好きではないデザインで生活を受け入れなければなりませんでした。ローカライズされた言語ごとにStringフィールドを追加しました。私たちは現在、いくつかの言語をサポートするだけで済むので、これは可能です。これにより、ローカライズされたテーブルが1つしかありませんでした。 JPAはクエリでローカライズされたテーブルに効率的に参加できますが、これは多くの言語ではうまく調整されず、任意の数の言語をサポートしません。メンテナンス性のために、私はLocalizedの外部インターフェイスを同じにしておき、実装をマップからフィールド単位の言語に変更したので、将来容易に切り替えることができます。

+1

投稿のおかげで:)興味深いコメントは、所有者を削除することの意味を再。私のユースケースでは、ローカライズされたものを共有しません。それぞれのテキストインスタンスに固有のローカライズされたものがあります。私は定数のような意味でそれらを使用していないので、複数の所有者に適用できます。私のユースケースはデータのロード時には常に比較的軽いので、効率の問題に気付かないことを願っています。 最終的に正規化を解除しなければならないのは残念ですか?そうしないと決心しました!私はまだ完成していないが、私はまだそれをやらなければならないかもしれない:D – IrishDubGuy

+0

BTW - 私はGWTアプリの文脈でこれをやっている。ソリューションはGWT-RPC接続を経由しません。私は結局これに対する解決策を見つけ、すぐにそれを掲示するでしょう – IrishDubGuy

関連する問題