Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Validation

Some data must not get into the database at all: a post without a title, an email without the @ sign. Jakarta Validation (Bean Validation) describes such rules with annotations on fields. Hibernate finds the validator on the classpath and checks an entity automatically before insert and update. The reference implementation is a separate project, Hibernate Validator.

Dependencies

Add two dependencies to pom.xml:

<!-- pom.xml -->
<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>9.1.4.Final</version>
</dependency>
<dependency>
  <groupId>org.glassfish.expressly</groupId>
  <artifactId>expressly</artifactId>
  <version>6.0.0</version>
</dependency>

The second library builds error messages from templates. Nothing has to be configured: Hibernate turns validation on as soon as it sees the validator.

Rules on fields

// src/main/java/com/example/blog/Post.java
package com.example.blog;
 
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
 
@Entity
public class Post {
 
    @Id
    @GeneratedValue
    private Long id;
 
    @NotBlank
    @Size(max = 200)
    private String title;
 
    @Size(min = 10, message = "the post is too short")
    private String body;
 
    @Email
    private String authorEmail;
 
    protected Post() {
    }
 
    public Post(String title, String body, String authorEmail) {
        this.title = title;
        this.body = body;
        this.authorEmail = authorEmail;
    }
}
AnnotationRule
@NotNullNot null
@NotBlankA string with at least one non-space character
@Size(min, max)Length of a string or size of a collection
@EmailLooks like an email address
@Min, @Max, @PositiveNumber limits
@Past, @FutureDates

The rules also affect the table: Hibernate creates title varchar(200) not null.

Automatic check

// src/main/java/com/example/blog/Main.java
emf.runInTransaction(em -> em.persist(new Post("Validation", "Checks before saving", "anna@example.com")));
IO.println("The first post is saved");
 
try {
    emf.runInTransaction(em -> em.persist(new Post(" ", "Short", "anna-at-example.com")));
} catch (RollbackException e) {
    if (e.getCause() instanceof ConstraintViolationException errors) {
        for (ConstraintViolation<?> v : errors.getConstraintViolations()) {
            IO.println(v.getPropertyPath() + ": " + v.getMessage());
        }
    }
}

The check runs when the changes are written, so the error arrives at commit, wrapped in RollbackException. Output:

Hibernate: insert into Post (authorEmail,body,title,id) values (?,?,?,?)
The first post is saved
Hibernate: select next value for Post_SEQ
title: must not be blank
body: the post is too short
authorEmail: must be a well-formed email address

The second post never reached the database: there is no insert for it. The order of the messages may differ.

Manual check

In an application you usually check the data earlier — right after reading a form — to show the errors to the user:

// src/main/java/com/example/blog/Check.java
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
    Validator validator = factory.getValidator();
    Post post = new Post("", "Short", "oleg@example.com");
 
    Set<ConstraintViolation<Post>> errors = validator.validate(post);
    IO.println("Errors: " + errors.size());
    errors.forEach(v -> IO.println(v.getPropertyPath() + ": " + v.getMessage()));
}
Errors: 2
title: must not be blank
body: the post is too short

The standard messages are translated: with a Russian locale the program prints the Russian texts. In Spring Boot the same annotations are checked by @Valid on a controller parameter.

Common mistake

Catching ConstraintViolationException around persist does not work: persist only remembers the object, and the check happens later, during flush or commit. Catch RollbackException around the transaction, as above, or call em.flush() inside a try to run the check right away.

Thanks for reading!

Donate to the site — we work hard for you.