Search code examples
javaspringvalidationbean-validation

custom jpa validation in spring boot


I have an entity like this:

@Entity
@Table(name = "transaction_receiver")
public class TransactionReceiver implements Serializable, Addressable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @NotNull
    @Column(name = "contact_id", nullable = false)
    private String contactId;

    @Column(name = "datev_number")
    private String datevNumber;

    @NotNull
    @Column(name = "receiver", nullable = false)
    private String receiver;

    @NotNull
    @Size(min = 22, max = 34)
    @Column(name = "iban", length = 34, nullable = false)
    private String iban;

    @Size(min = 8, max = 11)
    @Column(name = "bic", length = 11, nullable = false)
    private String bic;

    @NotNull
    @Column(name = "institute")
    private String institute;

    @Column(name = "company")
    private String company;

I need to write a custom validation "you can provide an empty iban, bic and institute, that's ok. But if any of the fields is not empty, the above constraint have to hold"

I am looking for the most elegant way to accomplish this.

My current solution is - what I think somehow dirty, but working - to use a @PrePersist statement and throw exceptions from there

    @PrePersist
    public void checkBankData() {
        boolean ibanEmpty = iban == null || iban.isEmpty();
        boolean ibanValid = !ibanEmpty && iban.length() >= 22 && iban.length() <= 34;
        boolean bicEmpty = bic == null || bic.isEmpty();
        boolean bicValid = !bicEmpty && bic.length() >= 8 && bic.length() <= 11;
        boolean instituteEmpty = institute == null || institute.isEmpty();

        boolean validState = (ibanEmpty && bicEmpty && instituteEmpty) || ibanValid && bicValid;

        if (!validState) {
            throw new IllegalStateException(
                String.format(
                    "bank data is not empty and %s%s%s%s%s",
                    !ibanValid ? "iban has to be from 22 to 34 chars long" : "",
                    !ibanValid && !bicValid ? "and" : "",
                    !bicValid ? "bic has to be from 8 to 11 chars long" : "",
                    !ibanValid && !bicValid && instituteEmpty ? "and" : "",
                    instituteEmpty ? "institue must not be empty" : ""
                )
            );
        }
    }

Which isn't subject to @Valid annotations. A different approach would be defining a custom validator like described here: http://docs.jboss.org/hibernate/validator/4.1/reference/en-US/html/validator-customconstraints.html

But this really looks like an overkill for my constraint.

Isn't there any other elegant way?


Solution

  • Using Hibernate Validation API is not as complex as it seems, and for your constraint is a nice solution. However you can get a easier way to define constrains using Hibernate Validator as we have done in one project adding a few classes. Your constraints will look like this:

    @Validate(method = "checkBankData", message = "{BankData.invalid.message}")
    @Entity
    @Table(name = "transaction_receiver")
    public class TransactionReceiver implements Serializable, Addressable {
    

    To get this you need to define @Validate annotation and a CustomValidator class.

    @Target({ ElementType.TYPE, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = CustomValidator.class)
    @Documented
    /**
     * Annotation to allow custom validation against model classes
     */
    public @interface Validate {
    
      /**
       * Validation message
       */
      String message();
    
      Class<?>[] groups() default {};
    
      Class<? extends Payload>[] payload() default {};
    
      /**
       * Validation method name
       */
      String method() default "";
    }
    
    
    public class CustomValidator implements ConstraintValidator<Validate, BusinessObject> {
    
      private static Log log = LogFactory.getLog(CustomValidator.class);
      private String validator;
    
    
      @Override
      public void initialize(Validate constraintAnnotation) {
        validator = constraintAnnotation.method();
      }
    
      @Override
      public boolean isValid(BusinessObject bo, ConstraintValidatorContext constraintContext) {
        try {
          return isValidForMethod(bo);
        } catch (Exception e) {
          /* Error durante la ejecución de la condición o del validador */
          log.error("Error validating "+bo, e);
          return false;
        }
      }
    
    
      private boolean isValidForMethod(BusinessObject bo) throws Exception {
        Method validatorMethod =  ReflectionUtils.findMethod(bo.getClass(), validator, new Class[] {});
        if (validatorMethod != null) {
          /* Validator call */
          Boolean valid = (Boolean) validatorMethod.invoke(bo);
          return valid != null && valid;
        } else {
          /* Method not found */
          log.error("Validator method not found.");
          return false;
        }
      }
    
    }
    

    This aproach will be nice if you plan to define more constraints. And you can extend it with more features like conditions for validation or adding multiple validations, etc.

    Off-topic:

    • Validation has nothing to do with Spring Boot so there is no need to mention it in your question.

    • serialVersionUID = 1L; Is a very bad idea. Use your IDE serialVersionUID generator to fill this field with a value different for 1L.