JavaEE7をはじめよう(26) - Bean ValidationとJPAの連携
前回は Bean ValidationのList
とgroups
を使って、複雑な制約条件を記述する方法を紹介した。今回は Bean Validation を組み込んだフレームワークの例として、JPA での使用例を紹介する。
JPA では、エンティティクラスに制約アノテーションを設定することで、自動的にエンティティに対してクラス単位のバリデーションを実行する( Bean Validation の jar ファイルがクラスパスにあれば、自動で実行する)。
具体的な実行タイミングは以下の3つである。
- 挿入時:
EntityManager#persist
(INSERT文)の実行時 - 更新時:データベースにUPDATE文を発行するタイミング
- 削除時:
EntityManager#remove
(DELETE文)の実行時
これにより、データベースの列定義上設定できない値(NotNull列へのnull
値設定や、最大桁数超えの文字列設定)をはじくことができるので、前段のチェックの不備ですり抜けた不正な値の登録を防止できる。
基本的なバリデーションの定義
まず、基本的なサンプルコードを以下に示す。
@Entity public class Person { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; // JPA定義 @Column(length = 40, nullable = false) // Bean Validation の制約定義 @NotNull @Size(max = 40) private String name; // JPA定義 @Column(nullable = true) // Bean Validation の制約定義 @Min(value = 10) private Integer age; // getter,setter, equals, hashCodeは省略 }
上記の定義では、name
列に対して、40文字以下かどうかをチェックすることを宣言している。またage
列に対しては NULL を許容し、入力値が10以上かどうかをチェックすることを宣言している。id
列は自動採番される値のため、チェックは省略している。
長さや NULL 許容などの制約について、JPA と Bean Validation で重複して定義していることに注目してほしい。
たとえば、name
列に対しては JPA の定義として@Column(length = 40, nullable = false)
を指定し、Bean Validationの定義として@NotNull
と@Size(max = 40)
を指定している。これらはどちらも NULL 許容や長さに関する制約だが、目的が異なる。JPA の定義は DDL 文生成などに利用し、Bean Validation の定義は実行時の入力値の妥当性チェックで利用するため、片方を省略することはできない。
これらの宣言をしておくことで、挿入・更新・削除それぞれのタイミングで自動的にチェックが実行される。
実行タイミングに応じてバリデーション内容を切り替える
アプリケーションによっては、挿入・更新・削除それぞれのタイミングで、バリデーション内容を変えたい場合もあるかもしれない。そんな場合には、Bean Validation のグループの仕組みを使って、挿入・更新・削除それぞれのタイミングでチェック内容を変更できる。
以下のコードでは、age
列について挿入時はnull
を許容し、更新・削除時はnull
を認めないように制御している。
@Entity public class Person { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; // JPA定義 @Column(length = 40, nullable = false) // 制約定義 nameの制約は常にチェック @NotNull(groups = {Default.class, PrePersist.class}) @Size(max = 40, groups = {Default.class, PrePersist.class}) private String name; @Column(nullable = true) // NotNullはデフォルトのみチェック(今回は更新・削除時対象) @NotNull @Min(value = 10,groups = {Default.class, PrePersist.class}) private Integer age; // getter,setter, equals, hashCodeは省略 }
挿入・更新・削除それぞれに対応するグループのクラスは設定ファイルで指定する。ここでは、挿入に対応するグループとしてPrePersist
を指定し、更新・削除に対応するグループとしてDefault
を設定している。
(グループの設定内容については次節で説明する。)
上記の例では、groups
にDefault
とPrePersist
を指定した制約はすべての場合にチェックし、Default
のみを指定した制約は更新・削除時にだけチェックすることになる。
そのため、age
列の@NotNull
は、更新・削除時にチェックし、その他の制約は挿入・更新・削除すべての場合でチェックを行う。
設定ファイル
バリデーションのグループ指定はpersistence.xml
で行う。
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> <persistence-unit name="..." transaction-type="..."> <!-- 割愛 --> <!-- NONEにすると、BeanValidationを無効にする --> <validation-mode>AUTO</validation-mode> <properties> <!-- ドライバ定義等 --> <!-- BeanValidationグループ設定 pre-perist,pre-update,pre-removeの3種類 未指定時は、javax.validation.groups.Defaultとなる。 --> <property name="javax.persistence.validation.group.pre-persist" value="entity.PrePersist"/> </properties> </persistence-unit>
バリデーションを行う3つのタイミングごとにグループを指定できる。
具体的には、以下の3つのプロパティのvalue
属性に対して、グループを表すインターフェースの完全クラス名を指定する(複数グループを指定する場合はカンマ区切りで指定する)。
- 挿入時:
javax.persistence.validation.group.pre-persist
- 更新時:
javax.persistence.validation.group.pre-update
- 削除時:
javax.persistence.validation.group.pre-remove
ここでは、pre-persist
属性に対してPrePersist
というグループを指定し、残りの2つはデフォルトにしている。
実行例
JUnit による実行例を示す。
public class JPAValidationGroupTest { private static EntityManager getEm() { return Persistence.createEntityManagerFactory("ut") .createEntityManager(); } EntityManager em; @Before public void setup(){ em = getEm(); em.getTransaction().begin(); } @After public void tearDown(){ em.getTransaction().rollback(); } @Test public void testValidationGroup(){ Person entity = new Person(); entity.setName("names"); // ageはnullのままでもpersistはOK try { em.persist(entity); } catch(ConstraintViolationException e) { throw e; } // insert実行 em.flush(); try { // nameを変更してUPDATE対象にする entity.setName("updated"); // バリデーションを通すには、ageの設定が必要。 // entity.setAge(10); em.flush(); fail("バリデーションエラーが起きない。"); } catch(ConstraintViolationException e) { assertThat(e.getConstraintViolations().size(), is(1)); } } }
この例では、INSERT時には属性age
がnull
でもOKだが、UPDATE時には違反となることを確認している。もちろん、INSERT時でも、そのほかの属性が制約に違反するようであれば、違反となる。
違反はConstraintViolationException
が発生することで検知する。この例外からは、違反となった情報であるConstraintVaiolation
のセットを取得できるため、実際に何が違反となったかを調べることができる。
まとめ
4回にわたって Bean Validation を解説した。
Bean Validation はアノテーションベースのシンプルな API で、Bean Validation 単体でも利用できる。JSF や Spring、JPA など、対応しているフレームワークも多く、拡張性も高いため、Java で入力チェックを行う場合には、有力な選択肢と言えるだろう。
[前多 賢太郎]