JPAでList <String>型のプロパティを永続化する方法は?


158

リスト型のフィールドが永続化されているエンティティを取得する最もスマートな方法は何ですか?

Command.java

package persistlistofstring;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Persistence;

@Entity
public class Command implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;
    @Basic
    List<String> arguments = new ArrayList<String>();

    public static void main(String[] args) {
        Command command = new Command();

        EntityManager em = Persistence
                .createEntityManagerFactory("pu")
                .createEntityManager();
        em.getTransaction().begin();
        em.persist(command);
        em.getTransaction().commit();
        em.close();

        System.out.println("Persisted with id=" + command.id);
    }
}

このコードは以下を生成します:

> Exception in thread "main" javax.persistence.PersistenceException: No Persistence provider for EntityManager named pu: Provider named oracle.toplink.essentials.PersistenceProvider threw unexpected exception at create EntityManagerFactory: 
> oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
> Local Exception Stack: 
> Exception [TOPLINK-30005] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException
> Exception Description: An exception was thrown while searching for persistence archives with ClassLoader: sun.misc.Launcher$AppClassLoader@11b86e7
> Internal Exception: javax.persistence.PersistenceException: Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.EntityManagerSetupException
> Exception Description: predeploy for PersistenceUnit [pu] failed.
> Internal Exception: Exception [TOPLINK-7155] (Oracle TopLink Essentials - 2.0.1 (Build b09d-fcs (12/06/2007))): oracle.toplink.essentials.exceptions.ValidationException
> Exception Description: The type [interface java.util.List] for the attribute [arguments] on the entity class [class persistlistofstring.Command] is not a valid type for a serialized mapping. The attribute type must implement the Serializable interface.
>         at oracle.toplink.essentials.exceptions.PersistenceUnitLoadingException.exceptionSearchingForPersistenceResources(PersistenceUnitLoadingException.java:143)
>         at oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider.createEntityManagerFactory(EntityManagerFactoryProvider.java:169)
>         at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:110)
>         at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:83)
>         at persistlistofstring.Command.main(Command.java:30)
> Caused by: 
> ...

回答:


197

いくつかのJPA 2実装を使用します。これは、Hibernateアノテーションと同様に、必要なことを正確に実行する@ElementCollectionアノテーションを追加します。ここに 1つの例があります

編集する

以下のコメントで述べられているように、正しいJPA 2実装は

javax.persistence.ElementCollection

@ElementCollection
Map<Key, Value> collection;

参照:http : //docs.oracle.com/javaee/6/api/javax/persistence/ElementCollection.html


1
私のミスは、@ OneToManyアノテーションも追加することでした...削除して@ ElementCollectionをそのままにした後、機能しました
Willi Mentzel

46

古いスレッドを復活させて申し訳ありませんが、文字列リストをデータベースの1つのフィールドとして保存する代替ソリューションを誰かが探している場合は、ここで私がそれを解決した方法を示します。次のようなコンバーターを作成します。

import java.util.Arrays;
import java.util.List;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
    private static final String SPLIT_CHAR = ";";

    @Override
    public String convertToDatabaseColumn(List<String> stringList) {
        return String.join(SPLIT_CHAR, stringList);
    }

    @Override
    public List<String> convertToEntityAttribute(String string) {
        return Arrays.asList(string.split(SPLIT_CHAR));
    }
}

次のようにエンティティで使用します。

@Convert(converter = StringListConverter.class)
private List<String> yourList;

データベースでは、リストはfoo; bar; foobarとして保存され、Javaオブジェクトではこれらの文字列を含むリストを取得します。

これが誰かに役立つことを願っています。


そのフィールドのコンテンツで結果をフィルタリングするために、JPAリポジトリと連携しますか?
Please_Dont_Bully_Me_SO_Lords

1
@Please_Dont_Bully_Me_SO_Lordsデータは「foo; bar; foobar」としてデータベースに格納されるため、このユースケースにはあまり適していません。データを照会したい場合は、おそらくElementCollection + JoinTableが状況に対応する方法です。
Jonck van der Kogel

これSPLIT_CHARは、文字列内に出現することはできないことも意味します。
クラッシュ

正しい@crush。もちろん、文字列を正しく区切った後に文字列をエンコードするなどして、それを許可することもできます。しかし、ここで投稿したソリューションは、主に単純なユースケースを対象としています。より複雑な状況のため、おそらくあなたはElementCollection + JoinTableとのより良い運賃う
コーゲルデルJonckバン

コードを修正してください。私はそれを「ライブラリコード」と見なすので、防御的である必要があります。たとえば、少なくともnullチェックが必要です
ZZ 5

30

この回答は、JPA2以前の実装で作成されました。JPA2を使用している場合は、上記のElementCollection回答を参照してください。

モデルオブジェクト内のオブジェクトのリストは、通常、別のオブジェクトとの「OneToMany」関係と見なされます。ただし、文字列はIDを持たないため、(それ自体では)1対多の関係の許容クライアントではありません。

したがって、文字列のリストを、IDと文字列を含むArgumentクラスのJPAオブジェクトのリストに変換する必要があります。文字列をIDとして使用できる可能性があります。これにより、IDフィールドの削除と文字列が等しい行の統合により、テーブルのスペースを少し節約できますが、引数を元の順序に戻すことができなくなります。 (注文情報を保存していなかったため)。

または、リストを@Transientに変換し、VARCHAR()またはCLOBのいずれかである別のフィールド(argStorage)をクラスに追加することもできます。次に、3つの関数を追加する必要があります。それらの2つは同じであり、文字列のリストを(argStorageで)簡単に分離できる方法で区切られた単一の文字列に変換する必要があります。@PrePersistと@PreUpdateを使用して、これら2つの関数(それぞれが同じことを行う)に注釈を付けます。最後に、argStorageを文字列のリストに再度分割する3番目の関数を追加し、@ PostLoadに注釈を付けます。これにより、コマンドを保存するときは常にCLOBが文字列で更新され、DBに保存する前にargStorageフィールドが更新されます。

私はまだ最初のケースを行うことをお勧めします。後で実際の関係を築くのに良い方法です。


ArrayList <String>から、カンマ区切りの値を持つ文字列に変更すると、うまくいきました。
Chris Dale、

2
しかし、これにより、そのフィールドをクエリするときに、(imho)醜いステートメントを使用する必要があります。
ウィスキーシエラ

はい、私が言ったように...最初のオプションを実行してください、それはより良いです。自分でそれを実行できない場合は、オプション2で十分です。
billjamesdev、2010年

15

HibernateでのJava Persistenceによると

注釈付きの値型のコレクションのマッピング[...]。執筆時点では、Java Persistence標準の一部ではありません。

Hibernateを使用している場合は、次のようなことができます。

@org.hibernate.annotations.CollectionOfElements(
    targetElement = java.lang.String.class
)
@JoinTable(
    name = "foo",
    joinColumns = @JoinColumn(name = "foo_id")
)
@org.hibernate.annotations.IndexColumn(
    name = "POSITION", base = 1
)
@Column(name = "baz", nullable = false)
private List<String> arguments = new ArrayList<String>();

更新:注、これは現在JPA2で使用可能です。


12

これも使えます。

@Column(name="arguments")
@ElementCollection(targetClass=String.class)
private List<String> arguments;

1
おそらく、@ JoinTableに加えて。
phil294

9

JPAのHibernate実装を使用しているとき、ListではなくArrayListとして型を宣言するだけでhibernateがデータのリストを格納できることがわかりました。

明らかに、これには、Entityオブジェクトのリストを作成する場合と比較して、いくつかの欠点があります。遅延読み込み、リスト内のエンティティを他のオブジェクトから参照する機能がないため、データベースクエリの構築がさらに困難になる可能性があります。ただし、エンティティと一緒に常に積極的にフェッチするかなりプリミティブなタイプのリストを処理している場合、このアプローチは私には問題ないと思われます。

@Entity
public class Command implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;

    ArrayList<String> arguments = new ArrayList<String>();


}

2
ありがとう。これはすべてのJPA実装で機能し、Arraylist is SerializableはBLOBフィールドに保存されます。この方法の問題は、1)BLOBサイズが固定されている2)配列要素を検索またはインデックス付けできる3)Javaシリアル化形式を認識しているクライアントだけがこれらの要素を読み取ることができることです。
Andrea Francia

このアプローチを試すと、サーバーの起動時に例外@OneToMany @ManyToOne @ElementCollectionCaused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElements発生します。hibernatesはコレクションインターフェイスを使用することを望んでいるからです。
Paramvir Singh Karwal

9

私は同じ問題を抱えていたので、与えられた可能な解決策に投資しましたが、最後に自分の「;」を実装することにしました。文字列の区切りリスト。

ので、私は持っています

// a ; separated list of arguments
String arguments;

public List<String> getArguments() {
    return Arrays.asList(arguments.split(";"));
}

このように、リストはデータベーステーブルで簡単に読み取り/編集できます。


1
これは完全に有効ですが、アプリケーションの成長とスキーマの進化を考慮してください。(近い)将来のある時点で、最終的にエンティティベースのアプローチに切り替える可能性があります。
ウイスキーシエラ

私は同意します、それは完全に有効です。ただし、ロジックとコードの実装を完全に確認することをお勧めします。String argumentsがアクセス許可のリストである場合、特殊文字a separatorがあると、特権昇格攻撃に対して脆弱になる可能性があります。
Thang Pham、2010

1
これは本当に悪いアドバイス;です。あなたの文字列はあなたのアプリを壊すでしょう。
agilob

9

回答のどれもが、 @ElementCollectionマッピングのです。

このアノテーションを使用してリストをマップし、JPA / Hibernateがテーブルや列などを自動生成するようにすると、自動生成された名前も使用されます。

それでは、基本的な例を分析しましょう:

@Entity
@Table(name = "sample")
public class MySample {

    @Id
    @GeneratedValue
    private Long id;

    @ElementCollection // 1
    @CollectionTable(name = "my_list", joinColumns = @JoinColumn(name = "id")) // 2
    @Column(name = "list") // 3
    private List<String> list;

}
  1. 基本的な@ElementCollection注釈(既知fetchおよびtargetClass設定を)
  2. @CollectionTableそれが生成されますテーブル、などのような定義に名前を与えることになると注釈は非常に便利であるjoinColumnsforeignKeyの、indexesuniqueConstraintsなど、
  3. @Columnvarcharリストの値を格納する列の名前を定義することが重要です。

生成されるDDL作成は次のようになります。

-- table sample
CREATE TABLE sample (
  id bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
);

-- table my_list
CREATE TABLE IF NOT EXISTS my_list (
  id bigint(20) NOT NULL,
  list varchar(255) DEFAULT NULL,
  FOREIGN KEY (id) REFERENCES sample (id)
);

4
TABLE構造を含む完全な説明を提供し、さまざまな注釈が必要な理由を説明する唯一の提案されたソリューションであるため、このソリューションが好きです。
ジュリアンクロネッグ

6

わかりました、少し遅れています。しかし、時間が経つにつれてこれを見る勇敢な魂にとっては。

ドキュメントに書かれているように

@Basic:データベース列へのマッピングの最も単純なタイプ。Basicアノテーションは、Javaプリミティブ型、[...]、列挙型、およびjava.io.Serializableを実装するその他の型の永続プロパティまたはインスタンス変数に適用できます。

重要な部分は、Serializableを実装する型です

したがって、これまでで最も簡単で使いやすいソリューションは、List(または任意のシリアル化可能なコンテナ)の代わりに単にArrayListを使用することです。

@Basic
ArrayList<Color> lovedColors;

@Basic
ArrayList<String> catNames;

ただし、これはシステムのシリアル化を使用するので、次のような代償が伴います。

  • シリアル化されたオブジェクトモデルが変更されると、データを復元できない可能性があります

  • 格納される要素ごとに小さなオーバーヘッドが追加されます。

要するに

フラグやいくつかの要素を保存するのは非常に簡単ですが、大きくなる可能性のあるデータを保存することはお勧めしません。


これを試しましたが、sqlテーブルはデータ型を小さなブロブにしました。これにより、文字列のリストの挿入と取得が非常に不便になりませんか?または、jpaは自動的にシリアライズおよびデシリアライズしますか?
Dzhao

3

チアゴの答えは正しいです。質問により具体的なサンプルを追加します。@ ElementCollectionはデータベースに新しいテーブルを作成しますが、2つのテーブルをマッピングしません。つまり、コレクションはエンティティのコレクションではなく、単純なタイプ(文字列など)のコレクションです。 。)または埋め込み可能な要素のコレクション(@Embeddableで注釈されたクラス)。

これは文字列のリストを永続化するサンプルです

@ElementCollection
private Collection<String> options = new ArrayList<String>();

カスタムオブジェクトのリストを永続化するサンプルを以下に示します

@Embedded
@ElementCollection
private Collection<Car> carList = new ArrayList<Car>();

この場合、クラスを埋め込み可能にする必要があります

@Embeddable
public class Car {
}

3

@ConverterとStringTokenizerを使用してセットを保存するためのソリューションを次に示します。@ jonck-van-der-kogelソリューションに対してもう少しチェックします。

エンティティクラスで:

@Convert(converter = StringSetConverter.class)
@Column
private Set<String> washSaleTickers;

StringSetConverter:

package com.model.domain.converters;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

@Converter
public class StringSetConverter implements AttributeConverter<Set<String>, String> {
    private final String GROUP_DELIMITER = "=IWILLNEVERHAPPEN=";

    @Override
    public String convertToDatabaseColumn(Set<String> stringList) {
        if (stringList == null) {
            return new String();
        }
        return String.join(GROUP_DELIMITER, stringList);
    }

    @Override
    public Set<String> convertToEntityAttribute(String string) {
        Set<String> resultingSet = new HashSet<>();
        StringTokenizer st = new StringTokenizer(string, GROUP_DELIMITER);
        while (st.hasMoreTokens())
            resultingSet.add(st.nextToken());
        return resultingSet;
    }
}

1

この問題に対する私の修正は、主キーを外部キーで分離することでした。Eclipseを使用していて、上記の変更を行った場合は、データベースエクスプローラーを更新してください。次に、テーブルからエンティティを再作成します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.