JavaEE7をはじめよう(8) - JPAでのID定義
前回まで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は省略。 }
注意すべきことは、主キーの一意性を保証するためにequals
とhashCode
を必ず定義しておくことだ。
次にエンティティクラスを定義する。エンティティクラスに複合主キーを設定するには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:@IdClass
でPK
クラスを宣言し、@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が好まれるようだ。
複合主キーの留意点と代替案
ここまで複合主キーの定義方法を説明した。単一主キーと比べて、複合主キーの定義は実装の負担となることが多い。
次の参考資料では主キーは単一である方が取り扱いが容易であると述べている。
Basic Java Persistence API Best Practices
同様のプラクティスは 書籍の Begining Java EE 6, Begining Java EE 7 にも記述されている。
複合主キーにすべきではない理由として、複合主キーの定義方法が煩雑なほか、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の定義方法は以下のいずれかで統一するべきだ。
- 全てのテーブルに代理キーを設定する
- 単一の自然キーを持つテーブルはそのキーをIDにし、複合主キーのテーブルには代理キーを適用することで、IDを全て単一にする
- 全て自然キー(複合主キーも含む)を使う
1番目の方法が最もシンプルで効率的だ。
(とはいえ、すでにテーブル構造が決まっていて、複合主キーを使用せざるを得ない場合もあるだろう。)
代理キーを使用する際の留意点を挙げておく。
一意制約を設定する
代理キーを使用するとしても、自然キーにあたる項目には一意制約を設定する。そうしないと、同時実行で自然キーのデータが重複するなどのデータ不整合を招く。
一意制約の設定方法は、前述の代理キー設定のサンプルコードにあるとおり、@Table
アノテーションのuniqueConstraints
属性に記述する。一意制約例外のハンドリングを考慮する
すでに存在するIDのエンティティをINSERT
しようとすると、JPAではEntityExistsException
が発生する。
ただし、上記の方法で一意制約を設定して一意制約違反となった場合、PersistenceException
が発生する。
このPersistenceException
は JPAの例外の基底クラスのため、その型からは問題の原因を判別できない。 もし、一意制約違反であることを識別する必要があるなら、PersistenceException
からSQLException
を取得し、SQLコード等から判定する必要がある。代理キーの値に依存したプログラムを書かない
代理キーの設定は@GeneratedValue
アノテーションでJPAに任せるべきである。その場合、どのようにIDが設定されるか制御できないし、環境移行などで異なるIDが付与される可能性がある。 よって、 例えば、代理キーのIDが100のデータを取得するといった、IDを定数で指定するプログラムを書かないようにする。
次回
次回はJPAのまとめとして、JPAを利用する上での各種Tipsを紹介する。
[前多 賢太郎]