JPA:大きな結果セットを反復するための適切なパターンは何ですか?


114

数百万行のテーブルがあるとしましょう。JPAを使用して、そのテーブルに対するクエリを反復処理する適切な方法は何ですか?何百万ものオブジェクトを持つすべてのメモリ内リストがないのはなぜですか?

たとえば、テーブルが大きい場合は、次のようになると思います。

List<Model> models = entityManager().createQuery("from Model m", Model.class).getResultList();

for (Model model : models)
{
     System.out.println(model.getId());
}

ページネーション(ループと手動更新setFirstResult()/ setMaxResult())は本当に最善の解決策ですか?

編集:私が対象としている主なユースケースは、一種のバッチジョブです。実行に時間がかかる場合は問題ありません。関連するWebクライアントはありません。一度に1つ(またはいくつかの小さなN)の行ごとに「何かを行う」必要があります。私はそれらすべてが同時にメモリにあるのを避けようとしているだけです。


どのデータベースとJDBCドライバーを使用していますか?

回答:


55

Java Persistence with Hibernateのページ537 は、を使用したソリューションを提供していますScrollableResultsが、残念ながらそれはHibernate専用です。

したがって、setFirstResult/ を使用setMaxResultsして手動で反復する必要があるようです。JPAを使用した私のソリューションは次のとおりです。

private List<Model> getAllModelsIterable(int offset, int max)
{
    return entityManager.createQuery("from Model m", Model.class).setFirstResult(offset).setMaxResults(max).getResultList();
}

次に、次のように使用します。

private void iterateAll()
{
    int offset = 0;

    List<Model> models;
    while ((models = Model.getAllModelsIterable(offset, 100)).size() > 0)
    {
        entityManager.getTransaction().begin();
        for (Model model : models)
        {
            log.info("do something with model: " + model.getId());
        }

        entityManager.flush();
        entityManager.clear();
        em.getTransaction().commit();
        offset += models.size();
    }
}

33
バッチ処理中に新しい挿入がある場合、この例は安全ではないと思います。ユーザーは、新しく挿入されたデータが結果リストの最後にあることが確実な列に基づいて注文する必要があります。
Balazs Zsoldos 2012年

現在のページが最後のページであり、要素数が100未満の場合、size() == 100代わりに空のリストを返す1つの追加クエリがスキップされます
cdalxndr

38

私はここに提示された答えを試しましたが、JBoss 5.1 + MySQL Connector / J 5.1.15 + Hibernate 3.3.2はそれらで動作しませんでした。JBoss 4.xからJBoss 5.1に移行したばかりなので、とりあえずそれを使い続けているため、使用できる最新のHibernateは3.3.2です。

いくつかの追加パラメーターを追加するとうまくいき、このようなコードはOOMEなしで実行されます。

        StatelessSession session = ((Session) entityManager.getDelegate()).getSessionFactory().openStatelessSession();

        Query query = session
                .createQuery("SELECT a FROM Address a WHERE .... ORDER BY a.id");
        query.setFetchSize(Integer.valueOf(1000));
        query.setReadOnly(true);
        query.setLockMode("a", LockMode.NONE);
        ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
        while (results.next()) {
            Address addr = (Address) results.get(0);
            // Do stuff
        }
        results.close();
        session.close();

重要な行は、createQueryとscrollの間のクエリパラメータです。それらがないと、「スクロール」コールはすべてをメモリにロードしようとし、決して終了しないか、OutOfMemoryErrorまで実行されます。


2
こんにちはZdsさん、何百万行もスキャンするというユースケースは確かに私には一般的です。そして、最終的なコードを投稿してくれてありがとう。私の場合、全文検索用にインデックスを付けるために、レコードをSolrに押し込みます。また、ここでは説明しないビジネスルールのため、JDBCまたはSolrの組み込みモジュールを使用するのではなく、Hibernateを経由する必要があります。
Mark Bennett、

お力になれて、嬉しいです :-)。大きなデータセットも扱っています。この場合、ユーザーは同じ都市/郡内、または場合によっては州内のすべてのストリート名をクエリできるため、インデックスを作成するには大量のデータを読み取る必要があります。
Zds

:あなたは本当にこれらすべてのフープを介して行かなければならないのMySQLが表示されますstackoverflow.com/a/20900045/32453(他のDBのは、あまり私が想像厳しいことかもしれません...)
rogerdpack

32

これをそのままのJPAで実際に行うことはできませんが、Hibernateはステートレスセッションとスクロール可能な結果セットをサポートしています。

私たちは日常的に数十億を処理していますはその助けを借りて、の行しています。

ドキュメントへのリンクは次のとおりです:http : //docs.jboss.org/hibernate/core/3.3/reference/en/html/batch.html#batch-statelesssession


17
ありがとう。誰かがHibernateを介して数十億行を実行していることを知るのは良いことです。ここの何人かの人々はそれは不可能だと主張しています。:-)
ジョージアームホールド

2
ここにも例を追加できますか?Zdsの例に似ていると思いますか?
rogerdpack 2016

19

正直に言うと、JPAを離れ、JDBCを使用することをお勧めします(ただし、 JdbcTemplateサポートクラスなど)。JPA(およびその他のORMプロバイダー/仕様)は、ロードされたすべてのものが一次キャッシュに留まる必要があると想定しているため、1つのトランザクション内で多くのオブジェクトを操作するようには設計されていません(そのため、clear() JPAでの)。

また、ORMのオーバーヘッド(反射は氷山の一角にすぎない)が非常に重要であり、プレーンに対して反復するため、より低レベルのソリューションをお勧めします。 ResultSet前述のような軽量サポートを使用していてもJdbcTemplateはるかに高速になるます。

JPAは、大量のエンティティに対して操作を実行するようには設計されていません。flush()/ clear()を使って回避することもできますがOutOfMemoryError、もう一度考えてみてください。莫大なリソース消費の代償を払うことはほとんどありません。


JPAの利点は、データベースにとらわれないだけでなく、従来のデータベース(NoSQL)も使用できない可能性があることです。フラッシュ/クリアを時々行うことは難しくありませんし、通常、バッチ操作はまれにしか行われません。
アダム・ゲント

1
こんにちはThomasz。JPA / Hibernateについて文句を言う理由はたくさんありますが、敬意を表して、それらが「多くのオブジェクトを操作するように設計されていない」ことを本当に疑っています。このユースケースの適切なパターンを学ぶ必要があるだけだと思います。
ジョージアームホールド

4
まあ、私は2つのパターンしか考えられません:ページネーション(数回言及)とflush()/ clear()。最初のものは、バッチ処理を目的として設計されていないIMHOですが、flush()/ clear()のシーケンスを使用すると、リークの多い抽象化のような匂いがします。
Tomasz Nurkiewicz

うん、それはあなたが言ったようにページネーションとフラッシュ/クリアの組み合わせでした。ありがとう!
ジョージアームホールド

7

EclipseLinkを使用している場合、このメソッドを使用して結果をIterableとして取得します

private static <T> Iterable<T> getResult(TypedQuery<T> query)
{
  //eclipseLink
  if(query instanceof JpaQuery) {
    JpaQuery<T> jQuery = (JpaQuery<T>) query;
    jQuery.setHint(QueryHints.RESULT_SET_TYPE, ResultSetType.ForwardOnly)
       .setHint(QueryHints.SCROLLABLE_CURSOR, true);

    final Cursor cursor = jQuery.getResultCursor();
    return new Iterable<T>()
    {     
      @SuppressWarnings("unchecked")
      @Override
      public Iterator<T> iterator()
      {
        return cursor;
      }
    }; 
   }
  return query.getResultList();  
}  

closeメソッド

static void closeCursor(Iterable<?> list)
{
  if (list.iterator() instanceof Cursor)
    {
      ((Cursor) list.iterator()).close();
    }
}

6
ニースのjQueryオブジェクト
USR-ローカルΕΨΗΕΛΩΝ

私はあなたのコードを試してみましたが、それでもOOMを取得します-すべてのTオブジェクト(およびTから参照されるすべての結合テーブルオブジェクト)は決してGCではないようです。プロファイリングは、org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWorkの「テーブル」からorg.eclipse.persistence.internal.identitymaps.CacheKeyとともに参照されていることを示しています。私はキャッシュを調べましたが、私の設定はすべてデフォルトです(選択を無効にする、ソフトサブキャッシュで弱める、キャッシュサイズ100、ドロップ無効化)。セッションを無効にする方法を調べ、それが役立つかどうかを確認します。ところで、「for(T o:results)」を使用して、単にリターンカーソルを反復処理します。
Edi Bice

Badum tssssssss
dctremblay

5

それはあなたがしなければならない操作の種類に依存します。何百万行以上ループするのですか?バッチモードで何かを更新していますか?すべてのレコードをクライアントに表示しますか?取得したエンティティの統計を計算していますか?

100万件のレコードをクライアントに表示する場合は、ユーザーインターフェイスを再検討してください。この場合、適切なソリューションは、結果にページ番号を付けsetFirstResult()setMaxResult()

大量のレコードの更新を起動した場合は、更新をシンプルに保ち、使用することをお勧めします Query.executeUpdate()。オプションで、メッセージ駆動型Beanまたはワークマネージャを使用して、非同期モードで更新を実行できます。

取得したエンティティの統計情報を計算している場合は、JPA仕様で定義されているグループ化関数を利用できます。

その他の場合は、具体的に説明してください:)


簡単に言うと、「行ごとに」何かを行う必要があります。確かにこれは一般的なユースケースです。現在取り組んでいる特定のケースでは、各行のID(PK)を使用して、完全にデータベースの外部にある外部Webサービスにクエリを実行する必要があります。結果はクライアントのWebブラウザに表示されないため、ユーザーインターフェースはありません。つまり、バッチジョブです。
ジョージアームホールド

各行に印刷IDが「必要」な場合、各行を取得し、IDを取得して印刷する以外に方法はありません。最適なソリューションは、何をする必要があるかによって異なります。
Dainius

@Caffeine Coma、各行のIDのみが必要な場合、最大の改善はおそらくその列をフェッチするだけでSELECT m.id FROM Model mあり、次にList <Integer>を反復処理することから得られます。
ヨルンホルストマン、2011

1
@JörnHorstmann-数百万行ある場合、それは本当に問題になりますか?私の要点は、何百万ものオブジェクト(ただし小さい)を含むArrayListは、JVMヒープには適さないということです。
ジョージアームホールド

@Dainius:私の質問は、「ArrayList全体をメモリ内に置かずに、どのようにして各行を反復処理できるか」です。言い換えれば、一度にNをプルするためのインターフェースが欲しいのですが、Nは100万よりかなり小さいです。:-)
ジョージアームホールド

5

これを実行するための「適切な」方法はありません。これは、JPAやJDO、またはその他のORMが意図するものではありません。少数の行を返すように構成できるので、ストレートJDBCが最善の代替手段になります。時間とそれらが使用されるときにそれらをフラッシュします。これがサーバー側カーソルが存在する理由です。

ORMツールは、一括処理用に設計されていません。オブジェクトを操作し、データが格納されているRDBMSをできるだけ透過的にするように設計されています。ほとんどは、少なくともある程度は透過部分で失敗します。この規模では、何十万もの行(オブジェクト)を処理する方法はなく、ORMを使用して何百万行も処理せず、オブジェクトのインスタンス化のオーバーヘッドが単純明快であるため、妥当な時間内に実行できます。

適切なツールを使用してください。まっすぐなJDBCとストアドプロシージャは、特にこれらのORMフレームワークと比較して、特に優れている点で、2011年に確実に存在します。

何百万ものものを単純なものに引っ張っても、それList<Integer>をどのように行うかに関係なく、あまり効率的ではありません。あなたが求めていることを行う正しい方法は、単純でSELECT id FROM table、に設定されSERVER SIDE(ベンダーに依存)、それにカーソルを合わせFORWARD_ONLY READ-ONLYて反復することです。

実際に何百万ものIDをプルして、それぞれのWebサーバーを呼び出して処理する場合、妥当な時間内にこれを実行するには、いくつかの並行処理も実行する必要があります。JDBCカーソルを使用してプルし、それらのいくつかを一度にConcurrentLinkedQueueに配置し、スレッドの小さなプール(#CPU / Cores + 1)をプルして処理することが、「メモリが不足している場合、通常のRAM容量。

この回答もご覧ください。


1
では、usersテーブルのすべての行にアクセスする必要のある会社はないということですか。彼らのプログラマーは、Hibernateをウィンドウの外に投げ出すだけです。「数十万行を処理する方法はありません」-私の質問では、setFirstResult / setMaxResultを指摘したので、明らかに方法あります。より良いものがあるかどうか私は尋ねています。
ジョージアームホールド

「単純なList <Integer>にさえ、何百万ものものをプルすることは、それをどのように行うかに関係なく、あまり効率的ではありません。」それがまさに私の主張です。巨大なリストを作成するのではなく、結果セットを反復する方法を尋ねてます。
ジョージアームホールド

答えで提案したように、SERVER_SIDEカーソルを使用したFORWARD_ONLY READ_ONLYを指定した単純なストレートJDBC selectステートメントを使用します。JDBCでSERVER_SIDEカーソルを使用する方法は、データベースドライバに依存します。

1
私はその答えに完全に同意します。最良の解決策は問題に依存します。問題がいくつかのエンティティを簡単にロードする場合は、JPAが適しています。問題が大量のデータを効率的に使用している場合は、直接JDBCの方が適しています。
extraneon

4
数百万のレコードをスキャンすることは、たとえば検索エンジンにインデックスを付けるなど、さまざまな理由で一般的です。そして、私は通常、JDBCがより直接的な経路であることに同意しますが、Hibernateレイヤーにバンドルされた非常に複雑なビジネスロジックがすでにあるプロジェクトにたどり着くことがあります。これをバイパスしてJDBCにアクセスすると、ビジネスロジックがバイパスされます。ビジネスロジックは、再実装して維持することが重要な場合があります。非定型のユースケースについて質問を投稿するとき、彼らはしばしばそれが少し奇妙であることを知っていますが、何かを継承するのか、ゼロから構築するのか、そして詳細を開示できないかもしれません。
Mark Bennett、

4

別の「トリック」を使用できます。関心のあるエンティティの識別子のコレクションのみをロードします。識別子のタイプがlong = 8bytesであるとすると、10 ^ 6のような識別子のリストは約8Mbになります。バッチプロセス(一度に1つのインスタンス)の場合は、耐えられます。次に、繰り返し処理を実行します。

もう一つの注意-特にレコードを変更する場合は特に、これをチャンクで行う必要があります。があります。、データベースのが大きくなります。

それはfirstResult / maxRowsの戦略を設定するために来るとき-それは次のようになります非常に非常に遠く上からの結果のために遅いです。

また、データベースが読み取りコミット分離で動作している可能性があることも考慮に入れてください。ファントム読み取りを回避するには、識別子を読み込み、エンティティを1つずつ(または10 x 10など)読み込みます。


こんにちは@Marcin、あなたまたは他の誰もが、できればJava8ストリームを使用して、このチャンク化されたid-firstの段階的なアプローチを適用するサンプルコードへのリンクを提供できますか?
krevelen

2

ここでの回答では、ストアドプロシージャの使用がそれほど目立たないことに驚きました。過去にこのようなことをしなければならなかったときに、小さなチャンクでデータを処理し、少しスリープしてから続行するストアドプロシージャを作成しました。スリープ状態にする理由は、おそらくWebサイトに接続されているなど、よりリアルタイムのクエリにも使用されているデータベースを圧倒しないためです。データベースを使用している人が他にいない場合は、スリープを省くことができます。各レコードを1回だけ処理する必要がある場合は、再起動しても復元できるように、処理したレコードを格納する追加のテーブル(またはフィールド)を作成する必要があります。

ここでのパフォーマンスの節約は大きく、おそらくJPA / Hibernate / AppServerランドで行うことができるものよりも桁違いに速く、データベースサーバーは、大規模な結果セットを効率的に処理するための独自のサーバー側カーソルタイプのメカニズムを備えている可能性があります。パフォーマンスの節約は、データベースサーバーからアプリケーションサーバーにデータを送信する必要がないため、データを処理し、それを返送する必要があります。

これを完全に排除できるストアドプロシージャを使用することには、いくつかの重大な欠点がありますが、個人のツールボックスでそのスキルを持ち、この種の状況でそれを使用できる場合、これらの種類のものをかなり迅速に打ち消すことができます。


1
-2投票-次の投票者はあなたの投票を守ってくれますか?
危険

1
これらを読みながら同じことを考えました。質問は、UIのない​​大量のバッチジョブを示しています。アプリサーバー固有のリソースが必要ない場合、なぜアプリサーバーを使用するのでしょうか。ストアドプロシージャの方がはるかに効率的です。
jdessey 14

@jdessey状況に応じて、インポート時にシステムの他の部分で何かを実行するインポート機能があるとします。たとえば、EJBとしてすでにコーディングされているビジネスルールに基づいて行を別のテーブルに追加します。次に、EJBを埋め込みモードで実行できない限り、アプリサーバーで実行する方が理にかなっています。
アルキメデストラハノ2015

1

@Tomasz Nurkiewiczの答えを拡張する。DataSource接続を提供できるへのアクセス権があります

@Resource(name = "myDataSource",
    lookup = "java:comp/DefaultDataSource")
private DataSource myDataSource;

あなたのコードでは

try (Connection connection = myDataSource.getConnection()) {
    // raw jdbc operations
}

これにより、インポート/エクスポートなどの特定の大規模なバッチ操作でJPAをバイパスできますが、必要に応じて他のJPA操作のエンティティマネージャーにアクセスできます。


0

Pagination概念を使用して結果を取得する


4
ページ付けはGUIに非常に適しています。しかし、大量のデータを処理するために、ScrollableResultSetはずっと前に発明されました。JPAにはありません。
extraneon

0

私はこれを自分で疑問に思いました。それは問題のようです:

  • データセットの大きさ(行)
  • 使用しているJPA実装
  • 各行に対してどのような処理を行っているか。

両方のアプローチ(findAllとfindEntries)を簡単に交換できるようにするために、イテレーターを作成しました。

両方試してみることをお勧めします。

Long count = entityManager().createQuery("select count(o) from Model o", Long.class).getSingleResult();
ChunkIterator<Model> it1 = new ChunkIterator<Model>(count, 2) {

    @Override
    public Iterator<Model> getChunk(long index, long chunkSize) {
        //Do your setFirst and setMax here and return an iterator.
    }

};

Iterator<Model> it2 = List<Model> models = entityManager().createQuery("from Model m", Model.class).getResultList().iterator();


public static abstract class ChunkIterator<T> 
    extends AbstractIterator<T> implements Iterable<T>{
    private Iterator<T> chunk;
    private Long count;
    private long index = 0;
    private long chunkSize = 100;

    public ChunkIterator(Long count, long chunkSize) {
        super();
        this.count = count;
        this.chunkSize = chunkSize;
    }

    public abstract Iterator<T> getChunk(long index, long chunkSize);

    @Override
    public Iterator<T> iterator() {
        return this;
    }

    @Override
    protected T computeNext() {
        if (count == 0) return endOfData();
        if (chunk != null && chunk.hasNext() == false && index >= count) 
            return endOfData();
        if (chunk == null || chunk.hasNext() == false) {
            chunk = getChunk(index, chunkSize);
            index += chunkSize;
        }
        if (chunk == null || chunk.hasNext() == false) 
            return endOfData();
        return chunk.next();
    }

}

私はチャンクイテレータを使用しなくなりました(そのため、テストされていない可能性があります)。ちなみに、使用したい場合はグーグルコレクションが必要になります。


「各行に対してどのような処理を行っているか」については、行数が数百万にある場合、id列のみの単純なオブジェクトでも問題が発生すると思われます。私もsetFirstResult / setMaxResultをラップする独自のイテレーターを書くことを考えましたが、これは一般的な(そしてうまくいけば解決される!)問題でなければならないことを理解しました。
ジョージアームホールド、2011

@Caffeine Coma Iteratorを投稿しましたが、おそらくこれに適応するJPAをさらに実行できます。それが役立つかどうか教えてください。最終的には使用しませんでした(findAllを実行した)。
アダム・ゲント

0

休止状態では、目的を達成するための4つの異なる方法があります。それぞれに設計上のトレードオフ、制限、結果があります。それぞれを調べて、どちらが状況に適しているかを判断することをお勧めします。

  1. scroll()でステートレスセッションを使用する
  2. すべての反復の後にsession.clear()を使用してください。他のエンティティをアタッチする必要がある場合は、それらを別のセッションでロードします。実際には、最初のセッションはステートレスセッションをエミュレートしますが、オブジェクトが切り離されるまで、ステートフルセッションのすべての機能を保持します。
  3. iterate()またはlist()を使用しますが、最初のクエリでIDのみを取得し、次に各反復の個別のセッションでsession.loadを実行して、反復の最後にセッションを閉じます。
  4. EntityManager.detach()別名Session.evict();でQuery.iterate()を使用します。

0

以下は、Kotlinでの単純なストレートJPAの例です。カーソルを使用せずに、任意の大きさの結果セットに対して一度に100アイテムのチャンクを読み取る方法を示しています(各カーソルはデータベース上のリソースを消費します)。キーセットのページ付けを使用します。

キーセットのページ付けの概念については、https://use-the-index-luke.com/no-offsetを参照してください。https://www.citusdata.com/blog/2016/03/30/five-ways-to- paginate /は、ページングのさまざまな方法とその欠点の比較に使用します。

/*
create table my_table(
  id int primary key, -- index will be created
  my_column varchar
)
*/

fun keysetPaginationExample() {
    var lastId = Integer.MIN_VALUE
    do {

        val someItems =
        myRepository.findTop100ByMyTableIdAfterOrderByMyTableId(lastId)

        if (someItems.isEmpty()) break

        lastId = someItems.last().myTableId

        for (item in someItems) {
          process(item)
        }

    } while (true)
}

0

オフセットを使用してサイズ要素を毎回フェッチするJPAおよびNativeQueryの例

public List<X> getXByFetching(int fetchSize) {
        int totalX = getTotalRows(Entity);
        List<X> result = new ArrayList<>();
        for (int offset = 0; offset < totalX; offset = offset + fetchSize) {
            EntityManager entityManager = getEntityManager();
            String sql = getSqlSelect(Entity) + " OFFSET " + offset + " ROWS";
            Query query = entityManager.createNativeQuery(sql, X.class);
            query.setMaxResults(fetchSize);
            result.addAll(query.getResultList());
            entityManager.flush();
            entityManager.clear();
        return result;
    }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.