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

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

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

JavaEE7をはじめよう(26) - Bean ValidationとJPAの連携

前回は Bean ValidationのListgroupsを使って、複雑な制約条件を記述する方法を紹介した。今回は 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を設定している。 (グループの設定内容については次節で説明する。)

上記の例では、groupsDefaultPrePersistを指定した制約はすべての場合にチェックし、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時には属性agenullでもOKだが、UPDATE時には違反となることを確認している。もちろん、INSERT時でも、そのほかの属性が制約に違反するようであれば、違反となる。

違反はConstraintViolationExceptionが発生することで検知する。この例外からは、違反となった情報であるConstraintVaiolationのセットを取得できるため、実際に何が違反となったかを調べることができる。

まとめ

4回にわたって Bean Validation を解説した。

Bean Validation はアノテーションベースのシンプルな API で、Bean Validation 単体でも利用できる。JSF や Spring、JPA など、対応しているフレームワークも多く、拡張性も高いため、Java で入力チェックを行う場合には、有力な選択肢と言えるだろう。

[前多 賢太郎]