1

I try to manually validate a graph of entities. I didn't have issues doing it when using Hibernate SessionFactory. Since I switched to Hibernate JPA the nested entities are not validated anymore. Why?

  • The Hibernate event-based validation is run with the default group and works at pre-persiste/pre-update/pre-remove phases, but a manual validation doesn't detect validation error in nested entities.
  • The whole entity graph is eagerly loaded so I assume the TraversableResolver is not the issue here. Anyway I still declared a custom TraversableResolver that always request the navigation to nested entities.
  • If I create a fresh entity graph in a unit test, outside of the persistence context, the validation error is found. Yet, if I detach the parent entity from the persistence context the validation error is still not found.

Any help understanding this issue would be greatly appreciated.

I'm using org.springframework.boot:spring-boot-starter-data-jpa (Spring Boot 2.1.7.RELEASE), packaged with Hibernate 5.3.10.Final. I'm also using Lombok.

Here is my code. Should the alwaysAnError field be present on the parent class a validation error will be found. If this field is nested in an @Valid child no error is found.

Parent.java

@Data @Builder @NoArgsConstructor @AllArgsConstructor
@Entity
@Table(name = "parent")
public class Parent {  
    [...]  

    @Valid
    @Builder.Default
    @OneToMany(mappedBy = "file", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
    @Fetch(value = FetchMode.SUBSELECT)
    private List<@Valid Child> children = new ArrayList<>();
}

Child.java

@Data @Builder @NoArgsConstructor @AllArgsConstructor
@Entity
@Table(name = "child")
public class Child {
    [...]  

    @ManyToOne
    @JoinColumn(name = "file_id")
    private File file;

    @NotNull
    @Transient
    private String alwaysAnError = null; 
}

ValidationService.java

Validator validator = Validation.byDefaultProvider()
    .configure()
    .traversableResolver(new TraversableResolver() {
        @Override
        public boolean isReachable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
            return true;
        }

       @Override
       public boolean isCascadable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
           return true;
       }
    })
    .buildValidatorFactory()
    .getValidator();

Set<ConstraintViolation<Declaration>> constraintViolations = validator.validate(fileInstance, Default.class);
if (!constraintViolations.isEmpty()) {
    throw new RuntimeException(constraintViolations);
}

Related questions:

Olivier Tonglet
  • 3,312
  • 24
  • 40

1 Answers1

2

I found the reason. The parent-child relation is declared with FetchType.EAGER but in my code the parent entity was already lazily loaded before reaching the validation step. Since Hibernate did already cache the entity it will retrieve the proxy and not an eagerly loaded instance.

From this post.

Hibernate does everything it can to have one and only one instance of an entity in the session.

The parent entity was already loaded from another relation.

@Entity
@Table(name = "lazy_child")
public class LazyChild {
    [...]  

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "file_id")
    private File file;
}

But that's not all of it. This is not a vanilla lazy-loading otherwise the TraversableResolver would have solved the issues and it does not explain why embedded children can't be validated either.

Looking at the debugger it seems some kind of byte code enhancement is made on the parent entity. Every field appears as null but can be accessed upon request (as long it's not the bean validation that does the request).
byte code enhancement

The entity is not just proxied but also modified in some way.

  • The relationships can't be traversed even with a yes-only TraversableResolver.
  • The embedded objects are ignored by the validator.

The solution is to not use Lombok but declare getters and annotate them with @Valid.

When lazy loaded associations are supposed to be validated it is recommended to place the constraint on the getter of the association.

See the Bean Validation documentation for the quote and this post for further references.

I created an issue in the Hibernate tracker and opened a second SO question requesting more explanation.

Olivier Tonglet
  • 3,312
  • 24
  • 40