Java EEのJPAは便利だけれど、使い方を間違えると性能問題が発生する。
代表的な問題と対策について調べてみました。
JPAを使うメリットとネイティブSQLを使うという選択肢
JPAを使うメリットは次の2つあるかと思います。
- 生産性・保守性向上
- RDBMSへの依存性排除
1は特に動的にSQLを生成する場合には大きなメリットとなります。
動的にSQLを生成する必要がなく、RDBMSを変更することがなく、かつ、新しい技術への好奇心がないメンバーの場合は、JPAを使わずにネイティブSQLを使った方がよいかもしれません。
代表的な問題と対策
N+1問題
問題
親子関係があるEntityで次のようにデータを取得すると、初めにparentsをとるために1回、ループの中でN回のSQLが発行されてしまう。
(生SQLならjoinで1回なのに)
List<Parent> parents = query(Parent).all();
for (Parent parent: parents) {
System.out.print(parent.child.name);
}
対策
JPAの場合、JPQLでJOIN FETCHを使用する。
SELECT p FROM Parent p JOIN FETCH p.children
のように記述すると、次のSQLが発行される。
SELECT p FROM Parent p LEFT OUTER JOIN FETCH p.children
他にも、@Fetchや@JoinFetchアノテーションを使用する方法もあるが、それぞれ、Hibernate, EclipseLink固有。
バルクinsert, update, delete問題
問題
大量のデータをループでmerge, removeしてしまい、大量のSQLが発行される。
解決策
JPAのbulk処理があるらしいが、これといった定番の方法がみつからない。
いずれにしても、まずは本当に大量データループで処理する必要があるのかを検討する。
ダメな場合は、DB固有のバルク処理にするのが現実的か。
クエリ発行箇所が特定できない問題
問題
DBで性能解析し、問題のあるSQLを特定しても、発行箇所はJPAなので、対応がわかりにくく、どこで発行されたSQLなのかわかりにくい。
解決策
ログに呼び出し元の情報(class, methodなど)とSQLを記録する。
インデックスつけ忘れ問題
問題
SQLやRDBの仕組みを知らなくてもプログラミングできてしまうため、indexの付け忘れがおきやすい。
解決策
レビューや単体性能テストで、インデックス付け忘れを防止する。
そのために、性能要件がシビアな部分は事前に洗い出し、単体性能テストの大量データを準備しておく。
※参考:アノテーションによるインデックス定義
@Entity
// @Tableアノテーションのindexesでインデックスを定義。
// coolumnListはテーブル側の列名でカンマ区切り
@Table(indexes = @Index(name = "member_index", columnList = "NAME,ID", unique = true ))
public class Member implements Serializable {
不要な大容量カラムを無意味に取得してしまう問題
問題
Blobやtextなど大容量のカラムがあり、それらが不要にも関わらず取得してしまい、性能問題になる。
解決策
プロパティの遅延ロードをする(@Basic(fetch = FetchType.LAZY))。
遅延ロードではデータが必要になって初めてSQLを発行する。
このため、トランザクションが終了している場合にはデータを取得できない点に注意が必要。
@Entity
public class ContainsBlob implements Serializable {
@Id
private String name;
@Column
@Lob
@Basic(fetch = FetchType.LAZY)
private byte[] largeData;
// 省略
}
また、できれば、blobやtextはできるだけ別テーブルにする。
それができなければ、必要なカラムだけを指定したView Tableを用意する。