LoginSignup
8
7

More than 5 years have passed since last update.

[Java1.8] bean-validationで相関バリデーションをしてみた

Last updated at Posted at 2015-09-15

概要

Spring Expression Languageを使って相関バリデーションしてみました。
相関バリデーションのような複雑なバリデーションルールを記述する方法としては、

  1. ELを使う方法
  2. Entityのなかに、valideメソッドを実装する方法

があるかとおもいますが、実際に書いてみました。

制約もスキーマ情報であること、制約条件は制約対象のできるだけ近くに記述されていること、宣言的であること、が大切だと思います。

さらに、groupsの使い方を確認してみました。
一般的にアノテーションのgroupsに設定するラベル的な使い方が紹介される事が多いと思いますが、ラベルとしてではなく実際にインターフェースとして使う方法です。

環境

  • JDK1.8

ライブラリ

  • javax-validation-api
  • spring-mvc
  • spring-context
  • hibernate-validation
  • ほか

参考

[JSR 303] :Bean Validation
Spring framework Reference document
oval object validation framework

実装

コード

ValidTrue.java
@Target(value={ TYPE,FIELD,METHOD })
@Retention(RUNTIME)
@Constraint(validatedBy={ SpELValidator.class })
public @interface ValidTrue{
    String value() default "true";
    String message() default "error!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
SpELValidator.java
public class SpELValidator implements ConstraintValidator<ValidTrue, Object>{
    ExpressionParser parser =  new SpelExpressionParser();
    ValidTrue annotation;

    @Override
    public void initialize(ValidTrue annotation) {
        this.annotation = annotation;
    }
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        context.buildConstraintViolationWithTemplate(annotation.message());

        EvaluationContext evContext = new StandardEvaluationContext(value);

        Expression exp = parser.parseExpression(annotation.value());
        return (boolean)exp.getValue(evContext, boolean.class);
    }
}

test

1. @ValidTrue type

Entity.java
@ValidTrue("from < to")
public class Entity{
    public LocalDate from;
    public LocalDate to;
}

2. @AsserTrue type

Entity.java
//@ValidTrue("from < to")
public class Entity{
    public LocalDate from;
    public LocalDate to;

    @AssertTrue
    private isValid(){ return from.isBefore(to); }
}
ValidTrueTest.java
public class ValidTrueTest {
    @Test
    public void test() throws Exception{
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        Entity e = new Entity();
        e.from = LocalDate.now();
        e.to = LocalDate.MIN;
        validator.validate(e).stream().peek(System.out::println).findFirst().ifPresent( Assert::assertNotNull );
    }
}

結果

ConstraintViolationImpl{interpolatedMessage='error!', propertyPath=, rootBeanClass=class com.test.batch.Entity, messageTemplate='error!'}

1の例は、SpELでなくても基本同じだと思いますが、ELの表現力の及ぶかぎりどんなバリデーションルールでもかけます。
booleanを返せばいいだけ。
2の例は標準のアノテーション@AssertTrueを用いるわけですが、メソッドとして定義する(インターフェースを変更してしまう)のが個人的にどうかと思ってしまいます。
なので、privateで宣言してみました。
これだと気になりません。
ところが・・・

groupsを使ってみる

test

1. @ValidTrue type

TypeA.java
@ValidTrue("from<to")
public interface TypeA  {   
}
TypeB.java
@ValidTrue("from>to")
public interface TypeB  {   
}
Entity.java
public class Entity implements TypeA,TypeB{
    public LocalDate from;
    public LocalDate to;
ValidTrueTest.java
public class ValidTrueTest {
    @Test
    public void test() throws Exception{
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        Entity e = new Entity();
        e.from = LocalDate.now();
        e.to = LocalDate.MIN;
        validator.validate(e,TypeA.class).stream().peek(System.out::println).findFirst().ifPresent( Assert::assertNotNull );
    }
}

2. @AssertTrue type

TypeA.java
public interface TypeA  {   
    public LocalDate getFrom();
    public LocalDate getTo();

    @AssertTrue
    public default boolean isValid(){
        return getFrom().isBefore(getTo());
    }
}
TypeB.java
public interface TypeB  {   
    public LocalDate getFrom();
    public LocalDate getTo();

    @AssertTrue
    public default boolean isValid(){
        return getFrom().isAfter(getTo());
    }
}
Entity.java
public class Entity implements TypeA,TypeB{
    @NotNull
    public LocalDate from;
    @NotNull
    public LocalDate to;

    @Override
    public LocalDate getFrom(){ return from; }
    @Override
    public LocalDate getTo(){ return to; }
ValidTrueTest.java
public class ValidTrueTest {
    @Test
    public void test() throws Exception{
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        Entity e = new Entity();
        e.from = LocalDate.now();
        e.to = LocalDate.MIN;
        validator.validate(e,Entity.class,TypeA.class).stream().peek(System.out::println).findFirst().ifPresent( Assert::assertNotNull );
    }
}

結果

制約条件が多いとき、一つのエンティティにTypeA用の制約とTypeB用の制約を沢山書くとごちゃごちゃするなら、このようにインターフェースに制約を分けて書くのは良い気がします。

仮面を張り替えるようなイメージでしょうか。
getter/setterもきちんと書いておけば、TypeA型として操作できますし。同じBeanだけど内容によって複数の意味に切り替えられます。

例)
* 未実効の申請 / 実行中の申請 / 実行済みの申請 (時間によって遷移する場合)
* 固定金利ローン / 変動金利ローン (そもそも複数のタイプがある場合)
などなど。

それぞれインターフェースを用意してvalidationを切り換えつつ、それぞれ別々のインターフェースを公開できます。この段階ならこの値がはいってないといけない、などなど。

結論

  • group便利!
  • ValidTrue悪くないと思うんだが・・・AssertTrueで良いといわれると・・・インターフェース自身に定義されていない要素もELだと引っ張ってこれるのは便利ですね。

Qiita初めての投稿になります。もし内容、書き方など問題があればご指摘くださいませ。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7