Hibernate を (JPA 経由で) 使用する、長時間実行される (しかし単純な) アプリケーションがあります。実行中にかなり劇的な減速が発生しました。entityManager.clear()
時折の電話を必要とするように絞り込むことができました。Hibernate のエンティティ マネージャーが 100,000 個のエンティティを追跡している場合、数個しか追跡していない場合よりも最大 100 倍遅くなります (以下の結果を参照)。 私の質問は、Hiberate が多くのエンティティを追跡しているときになぜそんなに遅くなるのかということです。それを回避する他の方法はありますか?
!!! 更新: これを Hibernate の自動フラッシュ コードに絞り込むことができました。!!!
具体的にはorg.hibernate.event.internal.AbstractFlushingEventListener
のflushEntities()
メソッド (少なくとも Hibernate 4.1.1.Final では)。その中には、永続化コンテキスト内のすべてのエンティティを反復処理するループがあり、それぞれのフラッシュに関する広範なチェックを実行します (私の例ではすべてのエンティティが既にフラッシュされていますが!)。
私の質問の2番目の部分に部分的に答えるとFlushModeType.COMMIT
、クエリでフラッシュモードを設定することでパフォーマンスの問題に対処できます(以下の更新された結果を参照)。例えば
Place place = em.createQuery("from Place where name = :name", Place.class)
.setParameter("name", name)
.setFlushMode(FlushModeType.COMMIT) // <-- yay!
.getSingleResult();
...しかし、これはかなり醜い解決策のように思えます-物事がフラッシュされたかどうかを知る責任を、更新メソッドに保持するのではなく、クエリメソッドに渡します。また、すべてのクエリ メソッドでフラッシュ モードを COMMIT に設定するか、EntityManager で設定する必要があることもほぼ意味します。
これは私を不思議に思います:これは予想される動作ですか? フラッシュやエンティティの定義方法に何か問題がありますか? それとも、これは Hibernate の制限 (またはおそらくバグ) ですか?
問題を特定するために使用したサンプル コードは次のとおりです。
テストエンティティ
@Entity @Table(name="place") @Immutable
public class Place {
private Long _id;
private String _name;
@Id @GeneratedValue
public Long getId() { return _id; }
public void setId(Long id) { _id = id; }
@Basic(optional=false) @Column(name="name", length=700,
updatable=false, nullable=false, unique=true,
columnDefinition="varchar(700) character set 'ascii' not null")
public String getName() { return _name; }
public void setName(String name) { _name = name; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { return getName().hashCode(); }
}
ベンチマーク コード
私が持っているテスト コードは、100000 個のランダムな地名を生成して挿入します。次に、そのうちの 5000 個を名前でランダムにクエリします。名前の欄に索引があります。
Place place = em.createQuery(
"select p from Place p where p.name = :name", Place.class)
.setParameter("name", name)
.getSingleResult();
比較のために、またそれがデータベース内のものではないことを確認するために、次の JDBC ベースのクエリ ( の下em.unwrap(Session.class).doWork(...)
) を別のランダムに選択された 5000 の地名に対して実行しました。
PreparedStatement ps = c.prepareStatement(
"select id, name from place where name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Place place = new Place();
place.setId(rs.getLong(1));
place.setName(rs.getString(2));
}
rs.close();
ps.close();
(注: ベンチマーク用の 5000 クエリごとに PreparedStatement を作成して閉じます)。
結果
以下のすべての結果は、5000 クエリの平均です。JVMが与えられました-Xmx1G
Seconds/Query Approach
0.000160s JDBC
0.000286s Hibernate calling clear() after import and every 100 queries
0.000653s Hibernate calling clear() once after the import
0.012533s Hibernate w/o calling clear() at all
0.000292s Hibernate w/o calling clear(), and with flush-mode COMMIT
その他の観察: Hibernate クエリ中 (クリア コールなし)、Java プロセスはコアを 100% 近くの使用率に固定しました。JVM が 500MB ヒープを超えたことはありません。クエリ中にも多くの GC アクティビティがありましたが、CPU 使用率は明らかに Hibernate コードによって支配されていました。