概要
Spring Expression Languageを使って相関バリデーションしてみました。
相関バリデーションのような複雑なバリデーションルールを記述する方法としては、
- ELを使う方法
- 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
実装
コード
@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 {};
}
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
@ValidTrue("from < to")
public class Entity{
public LocalDate from;
public LocalDate to;
}
2. @AsserTrue type
//@ValidTrue("from < to")
public class Entity{
public LocalDate from;
public LocalDate to;
@AssertTrue
private isValid(){ return from.isBefore(to); }
}
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
@ValidTrue("from<to")
public interface TypeA {
}
@ValidTrue("from>to")
public interface TypeB {
}
public class Entity implements TypeA,TypeB{
public LocalDate from;
public LocalDate to;
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
public interface TypeA {
public LocalDate getFrom();
public LocalDate getTo();
@AssertTrue
public default boolean isValid(){
return getFrom().isBefore(getTo());
}
}
public interface TypeB {
public LocalDate getFrom();
public LocalDate getTo();
@AssertTrue
public default boolean isValid(){
return getFrom().isAfter(getTo());
}
}
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; }
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初めての投稿になります。もし内容、書き方など問題があればご指摘くださいませ。