読者です 読者をやめる 読者になる 読者になる

エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaEE7をはじめよう(8) - JPAでのID定義

JavaEE JPA

前回までJPAの使用方法を解説してきた。今回は、JPAを利用する上で、テーブルの主キーおよびエンティティのIDの設計で考慮しておくべき事柄を説明する。

複合主キーの定義方法

IDの定義方法を検討をするために、まずJPAでの複合主キーの定義方法を解説する。 エンティティのIDは、永続性コンテキストの中でエンティティを一意に識別する値である。また、EntityManger#find の引数にIDを渡すことからわかるように、JPAでは ID は1つのオブジェクトでなければならない。

そのため、@Embeddable アノテーションを付与した複合主キーを表すクラスを定義する必要がある。(エンティティクラスの内部クラスとすることが多い)

@Embeddable
public static class PK {
    @Column(length = 10)
    private String name;
        
    @Column
    private int num;

    // getter,setter, equals, hashCodeは省略。
}

注意すべきことは、主キーの一意性を保証するためにequalshashCodeを必ず定義しておくことだ。

次にエンティティクラスを定義する。エンティティクラスに複合主キーを設定するには2通りの方法がある。

方法1:@EmbeddedId で複合主キーを定義する方法
@Entity
public class MultiPkTable1 implements Serializable {
    
    @EmbeddedId
    private PK pk;
    
    @Column
    private String other;

    // getter, setterは割愛
}

この方法では、PKクラスのフィールドをエンティティ上に定義する。そのため、PKクラスのプロパティ name にアクセスするには、 entity.getPk().getName() のようにプロパティをネストさせる必要がある。 同様に、JPQLにおいても、 e.pk.name のようにプロパティをネストさせる必要がある。

方法2:@IdClassPKクラスを宣言し、@Id複数設定する方法
@Entity
@IdClass(PK.class)
public class MultiPkTable2 implements Serializable {
    
    @Id
    private String name;
    
    @Id
    private int num;
    
    @Column
    private String other;

    // getter, setterは割愛
}

こちらの方法では、エンティティクラスに直接複合主キーのフィールドを定義し、@Id複数付与する。ただし、@Id を付与したフィールドは、@IdClassアノテーションに指定したクラス(ここでは PK.class)にあるものと、名前、型、フィールドの数が全て一致する必要がある。

方法1 と異なり、主キーのプロパティはネストしない。 そのため、nameプロパティにアクセスするにはentity.getName()と指定し、JPQLでは e.name と指定する。

IDClass アノテーションで 主キーのクラスを指定する必要があるのは、EntityManger#find の引数に渡すID の型を明確にする必要があるためだ。 どちらの方法であっても、 EntityManger#find メソッドを実行するためには以下のように、 PKクラスのオブジェクトを作る必要がある。

PK pk = new pk();
pk.setName("test");
pk.setNum(0);

MultiPkTable1 entity1 = em.find(pk);
MultiPkTable2 entity2 = em.find(pk);

以上、複合主キーを持つテーブルを定義する2つの方法について述べた。

一般的には、IDをソースコード上で明示できるため、@EmbeddedIdを利用する方法1が好まれるようだ。

複合主キーの留意点と代替案

ここまで複合主キーの定義方法を説明した。単一主キーと比べて、複合主キーの定義は実装の負担となることが多い。

JPAの指針でも、主キーは単一である方が取り扱いが容易であると述べている。
複合主キーにすべきではない理由として、複合主キーの定義方法が煩雑なほか、IDとなるオブジェクトのプロパティを変更できないことが挙げられている。
(管理状態のエンティティに対して、@Id, @EmbeddedId を付与されているプロパティを変更しようとすると、同期化の際に例外が発生する。)

複合主キーを避ける方法として、連番のようなユニークな値による代理キーを使用する方法がある。

以前の記事でも述べたが、@GeneratedValueアノテーションを用いることで、プログラムから操作せずにIDを自動的に設定できる。

@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"name", "num"}))
public class SurrogateKeyTable implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String name;
    
    @Column
    private int num;
}

IDの定義方法に一貫性を持たせる

IDの定義方法は以下のいずれかで統一するべきだ。

  1. 全てのテーブルに代理キーを設定する
  2. 単一の自然キーを持つテーブルはそのキーをIDにし、複合主キーのテーブルには代理キーを適用することで、IDを全て単一にする
  3. 全て自然キー(複合主キーも含む)を使う

1番目の方法が最もシンプルで効率的だ。
(とはいえ、すでにテーブル構造が決まっていて、複合主キーを使用せざるを得ない場合もあるだろう。)

代理キーを使用する際の留意点を挙げておく。

  • 一意制約を設定する
    代理キーを使用するとしても、自然キーにあたる項目には一意制約を設定する。そうしないと、同時実行で自然キーのデータが重複するなどのデータ不整合を招く。
    一意制約の設定方法は、前述の代理キー設定のサンプルコードにあるとおり、@TableアノテーションuniqueConstraints属性に記述する。

  • 一意制約例外のハンドリングを考慮する
    すでに存在するIDのエンティティをINSERTしようとすると、JPAではEntityExistsExceptionが発生する。
    ただし、上記の方法で一意制約を設定して一意制約違反となった場合、PersistenceExceptionが発生する。
    このPersistenceExceptionJPAの例外の基底クラスのため、その型からは問題の原因を判別できない。 もし、一意制約違反であることを識別する必要があるなら、PersistenceExceptionから SQLException を取得し、SQLコード等から判定する必要がある。

  • 代理キーの値に依存したプログラムを書かない
    代理キーの設定は@GeneratedValueアノテーションJPAに任せるべきである。その場合、どのようにIDが設定されるか制御できないし、環境移行などで異なるIDが付与される可能性がある。 よって、 例えば、代理キーのIDが100のデータを取得するといった、IDを定数で指定するプログラムを書かないようにする。

次回

次回JPAのまとめとして、JPAを利用する上での各種Tipsを紹介する。

[前多 賢太郎]