web-dev-qa-db-ja.com

JPA(または少なくともHibernate)で大規模なデータセットを処理する方法は?

Webアプリを非常に巨大なデータセットで動作させる必要があります。現時点では、OutOfMemoryExceptionまたは1〜2分で生成されている出力のいずれかが表示されます。

簡単に言えば、DBに2つのテーブルがあるとします。WorkerWorkLogで、最初のテーブルに約1000行、2番目のテーブルに10000000行あります。後者のテーブルには、「workerId」フィールドや「hoursWorked」フィールドなど、いくつかのフィールドがあります。必要なものは次のとおりです。

  1. 各ユーザーの合計作業時間をカウントします。

  2. 各ユーザーの作業期間のリスト。

プレーンSQLの各タスクの最も簡単なアプローチ(IMO)は次のとおりです。

1)

select Worker.name, sum(hoursWorked) from Worker, WorkLog 
   where Worker.id = WorkLog.workerId 
   group by Worker.name;

//results of this query should be transformed to Multimap<Worker, Long>

2)

select Worker.name, WorkLog.start, WorkLog.hoursWorked from Worker, WorkLog
   where Worker.id = WorkLog.workerId;

//results of this query should be transformed to Multimap<Worker, Period>
//if it was JDBC then it would be vitally 
//to set resultSet.setFetchSize (someSmallNumber), ~100

だから、私は2つの質問があります:

  1. jPA(または少なくともHibernate)を使用して各アプローチを実装する方法。
  2. この問題をどのように処理しますか(もちろんJPAまたはHibernateを使用)?
18
Roman

dBに2つのテーブルがあるとします。最初のテーブルに約1000行、2番目のテーブルに10000 000行のWorkerとWorkLogです。

このような大量の場合、Hibernateの StatelessSessionインターフェイス を使用することをお勧めします。

あるいは、Hibernateは、分離オブジェクトの形式でデータベースとの間でデータをストリーミングするために使用できるコマンド指向のAPIを提供します。 StatelessSessionには永続コンテキストが関連付けられておらず、高レベルのライフサイクルセマンティクスの多くを提供していません。特に、ステートレスセッションは、第1レベルのキャッシュを実装せず、第2レベルまたはクエリキャッシュと対話しません。トランザクションの後書きまたは自動ダーティチェックは実装されていません。ステートレスセッションを使用して実行される操作は、関連付けられたインスタンスにカスケードされることはありません。コレクションはステートレスセッションによって無視されます。ステートレスセッションを介して実行される操作は、Hibernateのイベントモデルとインターセプターをバイパスします。第1レベルのキャッシュがないため、ステートレスセッションはデータエイリアシングの影響を受けやすくなっています。ステートレスセッションは、基盤となるJDBCにはるかに近い低レベルの抽象化です。

_StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();

ScrollableResults customers = session.getNamedQuery("GetCustomers")
    .scroll(ScrollMode.FORWARD_ONLY);
while ( customers.next() ) {
    Customer customer = (Customer) customers.get(0);
    customer.updateStuff(...);
    session.update(customer);
}

tx.commit();
session.close();
_

このコード例では、クエリによって返されたCustomerインスタンスはすぐに切り離されます。それらは永続コンテキストに関連付けられることはありません。

StatelessSessionインターフェースによって定義されたinsert(), update()およびdelete()操作は、直接データベースの行レベルの操作と見なされます。その結果、SQL _INSERT, UPDATE_またはDELETEがそれぞれ即座に実行されます。それらは、Sessionインターフェースによって定義されたsave(), saveOrUpdate()およびdelete()操作とは異なるセマンティクスを持っています。

18
Pascal Thivent

EclipseLinkでもこれを実行できるようです。これを確認してください: http://wiki.Eclipse.org/EclipseLink/Examples/JPA/Pagination

Query query = em.createQuery...
query.setHint(QueryHints.CURSOR, true)
     .setHint(QueryHints.SCROLLABLE_CURSOR, true)
ScrollableCursor scrl = (ScrollableCursor)q.getSingleResult();
Object o = null;
while ((o = scrl.next()) != null) { ... }
3
lapsus63

この ブログ投稿 も役立ちます。ステートレスセッションでのアプローチを要約し、いくつかの追加のヒントを追加します。 JAX-RSを使用して結果をストリーミングする方法。

2
Tvaroh

メモリが制限されている大規模なデータセットのクエリを作成および操作するには、互いに組み合わせて使用​​する必要があるいくつかの手法があります。

  1. デフォルト(JDBC経由)は10であるため、setFetchSize(ある値、おそらく100+)を使用します。これはパフォーマンスに関するものであり、それに関連する最大の要因です。プロバイダー(Hibernateなど)から入手可能なqueryHintを使用してJPAで実行できます。 (何らかの理由で)JPA Query.setFetchSize(int)メソッドはないようです。
  2. 10K以上のレコードの結果セット全体をマーシャリングしようとしないでください。いくつかの戦略が適用されます。GUIの場合、ページングまたはページングを実行するフレームワークを使用します。 Luceneまたは商用の検索/インデックス作成エンジン(会社に資金がある場合はEndeca)を検討してください。データをどこかに送信するには、データをストリーミングし、Nレコードごとにバッファをフラッシュして、使用されるメモリ量を制限します。ストリームはファイルやネットワークなどにフラッシュされる場合があります。その下で、JPAはJDBCを使用し、JDBCは結果セットをサーバーに保持し、一度に行セットグループ内のN行のみをフェッチすることに注意してください。この内訳は、グループ内のデータのフラッシュを容易にするために操作できます。
  3. ユースケースが何であるかを検討してください。通常、アプリケーションは質問に答えようとしています。答えが10K以上の行を取り除くことである場合は、設計を確認する必要があります。繰り返しになりますが、Luceneのようなインデックスエンジンの使用を検討し、クエリを改良し、BloomFiltersを次のように使用することを検討してくださいcontainsキャッシュをチェックして、データベースにアクセスせずに干し草の山の中の針を見つけます。
1
Darrell Teague

生のSQLは最後の手段と見なされるべきではありません。データベース層ではなく、JPA層で物事を「標準」に保ちたい場合は、それでもオプションと見なす必要があります。 JPAは、ネイティブクエリもサポートしており、標準エンティティへのマッピングを引き続き実行します。

ただし、データベースで処理できない大きな結果セットがある場合は、JPA(標準)が大きなデータセットのストリーミングをサポートしていないため、実際にはプレーンJDBCを使用する必要があります。

JPAエンジンはアプリケーションサーバーに組み込まれており、どのJPAプロバイダーを使用するかを制御できない可能性があるため、JPA実装固有の構造を使用すると、異なるアプリケーションサーバー間でアプリケーションを移植することが難しくなります。

1

あなたが言及した特定のケースでは、データベースサーバーで計算を行うことが最善の選択肢であることに同意します。 HQLとJPAQLは、これらのクエリの両方を処理できます。

1)

_select w, sum(wl.hoursWorked) 
from Worker w, WorkLog wl
where w.id = wl.workerId 
group by w
_

または、関連付けがマップされている場合:

_select w, sum(wl.hoursWorked) 
from Worker w join w.workLogs wl
group by w
_

両方、またはObject []がWorkerとLongであるリストを返します。または、「動的インスタンス化」クエリを使用してそれをまとめることもできます。次に例を示します。

_select new WorkerTotal( select w, sum(wl.hoursWorked) )
from Worker w join w.workLogs wl
group by w
_

または(必要に応じて)おそらくただ:

_select new WorkerTotal( select w.id, w.name, sum(wl.hoursWorked) )
from Worker w join w.workLogs wl
group by w.id, w.name
_

WorkerTotalは単なるクラスです。一致するコンストラクターが必要です。

2)

_select w, new Period( wl.start, wl.hoursWorked )
from Worker w join w.workLogs wl
_

これにより、WorkLogテーブルの各行の結果が返されます... new Period(...)ビットは「動的インスタンス化」と呼ばれ、結果のタプルをオブジェクトにラップするために使用されます(消費が容易)。

操作と一般的な使用法については、Pascalが指摘しているようにStatelessSessionをお勧めします。

0
Steve Ebersole

私はこのようなものを使用していますが、非常に高速に動作します。また、アプリケーションはどのデータベースでも機能するはずなので、ネイティブSQLを使用するのも嫌いです。

次の結果は非常に最適化されたSQLになり、マップであるレコードのリストを返します。

String hql = "select distinct " +
            "t.uuid as uuid, t.title as title, t.code as code, t.date as date, t.dueDate as dueDate, " +
            "t.startDate as startDate, t.endDate as endDate, t.constraintDate as constraintDate, t.closureDate as closureDate, t.creationDate as creationDate, " +
            "sc.category as category, sp.priority as priority, sd.difficulty as difficulty, t.progress as progress, st.type as type, " +
            "ss.status as status, ss.color as rowColor, (p.rKey || ' ' || p.name) as project, ps.status as projectstatus, (r.code || ' ' || r.title) as requirement, " +
            "t.estimate as estimate, w.title as workgroup, o.name || ' ' || o.surname as owner, " +
            "ROUND(sum(COALESCE(a.duration, 0)) * 100 / case when ((COALESCE(t.estimate, 0) * COALESCE(t.progress, 0)) = 0) then 1 else (COALESCE(t.estimate, 0) * COALESCE(t.progress, 0)) end, 2) as factor " +
            "from " + Task.class.getName() + " t " +
            "left join t.category sc " +
            "left join t.priority sp " +
            "left join t.difficulty sd " +
            "left join t.taskType st " +
            "left join t.status ss " +
            "left join t.project p " +
            "left join t.owner o " +
            "left join t.workgroup w " +
            "left join p.status ps " +
            "left join t.requirement r " +
            "left join p.status sps " +
            "left join t.iterationTasks it " +
            "left join t.taskActivities a " +
            "left join it.iteration i " +
            "where sps.active = true and " +
            "ss.done = false and " +
            "(i.uuid <> :iterationUuid or it.uuid is null) " + filterHql +
            "group by t.uuid, t.title, t.code, t.date, t.dueDate, " +
            "t.startDate, t.endDate, t.constraintDate, t.closureDate, t.creationDate, " +
            "sc.category, sp.priority, sd.difficulty, t.progress, st.type, " +
            "ss.status, ss.color, p.rKey, p.name, ps.status, r.code, r.title, " +
            "t.estimate, w.title, o.name, o.surname " + sortHql;

    if (logger.isDebugEnabled()) {
        logger.debug("Executing hql: " + hql );
    }

    Query query =  hibernateTemplate.getSessionFactory().getCurrentSession().getSession(EntityMode.MAP).createQuery(hql);
    for(String key: filterValues.keySet()) {
        Object valueSet = filterValues.get(key);

        if (logger.isDebugEnabled()) {
            logger.debug("Setting query parameter for " + key );
        }

        if (valueSet instanceof Java.util.Collection<?>) {
            query.setParameterList(key, (Collection)filterValues.get(key));
        } else {
            query.setParameter(key, filterValues.get(key));
        }
    }       
    query.setString("iterationUuid", iteration.getUuid());
    query.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);

    if (logger.isDebugEnabled()) {
        logger.debug("Query building complete.");
        logger.debug("SQL: " + query.getQueryString());
    }

    return query.list();
0
Bojan Kraut