JPAのパフォーマンス改善

使い方によってパフォーマンスが大きく変わる。こういう使い方が駄目というわけではなく、場合によっては別の方法を試した方が良いという例。

1.問題点

[YOMOU CRAWLER] 第2回 クラス図の作成」にある通りNovelには複数のHistoryがあって、更新のある度に追加保存している。

// 1.Novelのエンティティ
public class Novel extends BaseObject implements Serializable {

    /** 小説の更新履歴セット */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "novel", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<NovelHistory> novelHistories = new HashSet<>();

    public void addNovelHistory(NovelHistory novelHistory) {
        novelHistories.add(novelHistory);
        novelHistory.setNovel(this);
    }
// 2.Novelの更新履歴のエンティティ
public class NovelHistory extends BaseObject implements Serializable {

    /** 小説 */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "novel_id")
    private Novel novel;
// 3.使用箇所
// Novelオブジェクトは永続化済み
if (novelHistory != null) {
    // 小説の更新履歴が作成された場合
    novel.addNovelHistory(novelHistory);
}

このように永続化された状態のオブジェクトにAddするだけで自動的にInsert文が発行されるため、Javaのビジネスロジック開発に集中することが出来る。通常はこれで問題ないのだが、NovelHistoryに既に大量のデータが保存されているとパフォーマンスが問題になる。

FetchType.LAZYを指定しているため、novelHistories.add(novelHistory)を実行するときに関連するHistoryがデータベースからSelectされ、novelHistories変数に格納される。上記の例ではInsert前に以下のようなSQLが実行されている。

select 省略 from novel_history where novel_id = ?;

つまり、1件追加したいだけなのに関連する全てのHistoryをSelectしてしまっている。何千件もHistoryがあればそれだけでパフォーマンスが悪化する。

2.回避策

今回は以下の様に修正してこの問題を回避した。

// 3’.使用箇所
if (novelHistory != null) {
    // 小説の更新履歴が作成された場合
    novelHistory.setNovel(novel);
}

novelHistoryは永続化されていないため、適切な箇所でsaveする必要があるが、こうすれば単純にInsert文のみ発行されるようになる。

HibernateでMultipleBagFetchExceptionが発生する

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags

MultipleBagFetchExceptionがHibernateによってスローされる理由は、重複が発生する可能性があり、Hibernate用語でBagと呼ばれる順序付けられていないListが重複を削除することを想定していないため。

@Entity
@Table(name = "app_user")
public class User implements Serializable {

    @OneToMany(fetch = FetchType.LAZY, mappedBy="user", cascade = CascadeType.ALL)
    private List<UserNovelInfo> userNovelInfos = new ArrayList<>();
@Entity
@Table(name = "user_novel_info")
public class UserNovelInfo extends BaseObject implements Serializable {

    /** ユーザー */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "username")
    private User user;

Listを型に使用すると内部的には大体Bagが使用される。
重複が発生する可能性を除去出来ないのであれば、Setを使用するように変更することで例外は発生しなくなる。

    @OneToMany(fetch = FetchType.LAZY, mappedBy="user", cascade = CascadeType.ALL)
    private Set<UserNovelInfo> userNovelInfos = new HashSet<>();

JPA応用編:列挙型とN:Nで紐付ける

以下の3点を通常のカラムのアノテーションの他に追加する必要がある。
@CollectionTableを使って関連テーブルとカラムを指定する。
@Enumeratedで永続化フィールドを列挙型として指定する。
@ElementCollectionでEntityではないクラスも使用できるようにする。

@Entity
@Table(name = "app_user")
public class User implements Serializable {

    /** 権限 */
    @Column(nullable = false)
    @CollectionTable(name = "app_user_roles", joinColumns = @JoinColumn(name = "username"))
    @Enumerated(EnumType.STRING)
    @ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
    private List<Role> roles;
/**
 * 権限
 */
public enum Role implements Serializable {

    /** 管理者 */
    ROLE_ADMIN,

    /** 一般 */
    ROLE_USER;
}
create table app_user (
    username varchar(16) not null,
    primary key (username)
);

create table app_user_roles (
    username varchar(16) not null,
    roles varchar(16) not null,
    primary key (username, roles)
);

[YOMOU CRAWLER] 第3回 エンティティの関連を作成する

前回作成したクラス図に従ってエンティティに関連を追加していく。
以下ソースコード抜粋。

@Entity
@Table(name = "app_user")
public class User implements Serializable {

    /** ユーザーの小説の付随情報 */
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<UserNovelInfo> userNovelInfos;
@Entity
@Table(name = "user_novel_info")
public class UserNovelInfo extends BaseObject implements Serializable {

    /** ユーザー */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "username")
    private User user;

    /** 小説 */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "novel_id")
    private Novel novel;
}
@Entity
@Table(name = "novel")
public class Novel extends BaseObject implements Serializable {

    /** ユーザーの小説の付随情報 */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "novel", cascade = CascadeType.ALL)
    private Set<UserNovelInfo> userNovelInfo = new HashSet<>();

@OneToManyで1:Nの関係を表している。@ManyToOneはその逆にN:1の関係を表している。@JoinColumnで外部キーのカラムを指定する。なお、このnameは外部キーのカラムの名が参照のリレーションシップのプロパティの名または参照元のエンティティまたは埋め込み可能クラスのフィールド + “_” + 参照される主キーのカラムの名となっている場合は省略可能となるが、逆にわかり難いので省略しないようにしている。