JPAの性能問題と対策


Java EEのJPAは便利だけれど、使い方を間違えると性能問題が発生する。
代表的な問題と対策について調べてみました。

JPAを使うメリットとネイティブSQLを使うという選択肢

JPAを使うメリットは次の2つあるかと思います。

  1. 生産性・保守性向上
  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を用意する。

参考にした情報