JPAを使う始めていろいろノウハウがたまってきたのでメモしておく。
はじめに
JPAを使うとSQLを書かなくて済んだり、RDBMSの違いを
意識しなくて良い場合もあるが、SQLやRDBMSを学習しなくて
よいという訳ではないことに注意。
前提
- JPA 2.0
- Hibernate 4.X
を想定。
他のプロバイダのことは考えていない。極力JPA標準の仕様を使うが、便利であればHibernate実装依存のものも使う(ノウハウのほとんどはHibernate依存)。
自分はSpring+JPA(Hibernate)を使っているので、JavaEE6の場合とは少し違うかも。
SQL Logging
開発中は必ずSQLログを出力すること。意図せぬSQLの発行や、flush忘れ等に気づける。
- hibeneteの場合、persistence.xmlに
<property name="hibernate.show_sql" value="true" /><property name="hibernate.format_sql" value="true" /><property name="hibernate.use_sql_comments" value="true" />
- 標準ログよりlog4jdbcを使った方がよい。
Commit Log
SQLだけではなく、トランザクションのコミットの有無もログに出しておくとトランザクションの設定ミスに気づける。
<!-- for hibernate3 -->
<logger name="org.hibernate.transaction">
<level value="debug" />
</logger>
<!-- for hibernate4 -->
<logger name="org.hibernate.engine.transaction">
<level value="debug" />
</logger>
LazyLoad
関連に関しては基本的にLazyにしておく。@ManyToOne、@OneToOneはデフォルトでEAGERなので@ManyToOne(fetch=FetchType.LAZY)を指定してLazyに変える。@Lobなど使用頻度の低いフィールドにも@Basic(fetch=FetchType.LAZY)を付ける。
- OpenEntityManagerInViewFilterパターン
- ExtendedPersistenceContext
EAGERでフェッチしたい場合はQueryで実現する。
- N+1 Selects ProblemにはFETCH JOINかReportingで説明するsummary objectを使用して対処。
example
// SELECT * FROM Posts
List<Post> posts = entityManager.createQuery("SELECT x from Post x", Post.class)
.getResultList();
for (Post post : posts) {
// lazy loading of comments list causes:
// SELECT * FROM Comments where post_id = @p0
for (Comment comment : post.getComments()) {
//print comment...
}
}
fetch joinを使って書き直す
// SELECT * FROM Posts x LEFT JOIN Comments c ON c.post_id = x.id
List<Post> posts = entityManager.createQuery("SELECT DISTINCT x From Post x FETCH JOIN x.comments", Post.class)
.getResultList();
Reporting
検索や一覧表示で複数のエンティティをジョインした結果を出力する必要がある場合、summary object(必要なフィールドだけを持つPOJO=DTO)の使用を検討すること。必要なフィールドだけ取得できるので、性能劣化を抑えることが出来る。
コンストラクタ式
fetch joinを二回以上する場合は要検討。
WARNING: firstResult/maxResults specified with collection fetch; applying in memory!
が出てたら注意。全件を取得した後にメモリ上でページングしているため。この場合コンストラクタ式でsummary objectを作成すること。
Batch Update
JPQL Batch Update
List<User> users = entityManager.createQuery("SELCT x FROM User");
for (User user : users) {
user.setActive(false);
}
entityManager.flush();
ユーザー数だけupdate文が発行される。
entityManager.createQuery("UPDATE User SET active = false").executeUpdate();
ただし、楽観ロック用の@Versionの更新は行われない点に注意。
JDBC Batch Update
for (int i = 0; i < N; i++) {
User u = new User(i , "name" + i);
entityManager.save(u);
}
entityManager.flush();
一件ずつ insert(Statement#executeUpdate)の代わりに、複数件まとめてinsert(Statement#executeBatch)させる。Hibernateの場合、hibernate.jdbc.batch_sizeに0より大きい値を設定すればexecuteBatchになる。
<property name="hibernate.jdbc.batch_size" value="30" />
という設定すれば30件ずつexecuteBatch が実行されるので、
N=3000の場合、SQL発行回数は100回に抑えられる。
Robustness
Queryを書く場合はNamed Query推奨。起動時にクエリがコンパイルされるので性能も良いし、文法ミスを検出できる。またSQLインジェクションを防ぐ。
保守性を高めるためにxmlファイルに記述するのが良いとおもう。persistence.xmlに
<mapping-file>xxx/yyy/ItemOrm.xml</mapping-file>
エンティティ毎(Repository/DAO毎)にxmlを作成するのが良い。
Queryのnameはできれば定数にしたい。xmlから定数クラスを自動生成するツールを作ったので、今度公開する。
org.hibernate.cfg.annotations.QueryBinderカテゴリをdebugレベルにしておくと、起動時にNamedQuery一覧を出力できる。
<property name="hibernate.use_sql_comments" value="true" />
を設定しておくと、NamedQueryのnameがコメントに付加されるため、
DB側からのトレーサビリティが高くなる。
動的クエリはCriteriaQueryで良いが読みにくい。。
Tips
Hibernate専用であるが、
@Whereをつけると参照SQLのWHERE句に常に特定の条件をつけることができる。
@Where(clause = "active = true")
@Entity
public class User {
@Id
private Integer id;
@Column
private String name;
@Column
private boolean active;
// setter/getter...
}