Validation: Difference between ORM validation and Validator validation

EntityA has many related EntityB. EntityB can only be related to EntityA if field value matches:

@Entity
public class EntityA {
    @Id
    @GeneratedValue
    public Long id;

    @NotNull
    public String field;

    @NotNull
    @OneToMany(mappedBy = "entityA", cascade = CascadeType.ALL)
    public List<@Valid EntityB> entityB;
}

@EntityBConstraint
@Entity
public class EntityB {
    @Id
    @GeneratedValue
    public Long id;

    @NotNull
    public String field;

    @NotNull
    @ManyToOne
    public EntityA entityA;
}
public class EntityBValidator implements ConstraintValidator<EntityBConstraint, EntityB> {
    @Override
    public boolean isValid(EntityB entityB, ConstraintValidatorContext constraintValidatorContext) {
        return entityB.field.equals(entityB.entityA.field);
    }
}

Test:

    @Test
    public void test() {
        EntityA entityA = new EntityA();
        entityA.field = "field-value";

        EntityB entityB = new EntityB();
        entityB.field = "field-value";
        entityB.entityA = entityA;
        entityA.entityB = List.of(entityB);

        entityService.createEntityA(entityA);

        entityA.field = "update";
        entityA = entityService.updateEntityA(entityA);                    (1)

        validator.validate(entityA).forEach(System.out::println);          (2)
    }

When we update entityA.field, entityB is not validated (1).

But if we validate entityA explicitly (2) we see the expected violation:

ConstraintViolationImpl{
  interpolatedMessage='{org.acme.EntityBConstraint.message}', 
  propertyPath=entityB[0], 
  rootBeanClass=class org.acme.EntityA, 
  messageTemplate='{org.acme.EntityBConstraint.message}'
}

Only changed (dirty) entities get validated when interacting with the EntityManager. This is not obivous and well documented - at least we could not find it.

How would you handle this without manually calling Validator.validate?

Related comment: Hibernate @OneToMany removing from Set doesn't trigger constraint validation on update of parent entity - #7 by beikov

If the relation is bidirectional like in the example above you could define the class level constraint on both sides?

But what to do with unidirectional relations?

@EntityBConstraint
@Entity
public class EntityB {
    @Id
    @GeneratedValue
    public Long id;

    @NotNull
    public String field;

    @Valid
    @ManyToOne(cascade = CascadeType.ALL)
    public EntityC entityC;
}

@Entity
public class EntityC {
    @Id
    @GeneratedValue
    public Long id;

    public String field;
}
public class EntityBValidator implements ConstraintValidator<EntityBConstraint, EntityB> {

    @Override
    public boolean isValid(EntityB entityB, ConstraintValidatorContext constraintValidatorContext) {
        return entityB.field.equals(entityB.entityC.field);
    }
}

A potential EntityCValidator could not access EntityB..

We know of this limitation, but don’t have a good answer yet. Also see Jira

Thanks for your reply.

Jira seems to be about collections. This issue is about relations and their validation in general.

It’s about collections, yeah, just like in your example. In general, right now Hibernate ORM validates only objects that changed. Ideally, it would validate the “owning entity” of objects that changed i.e. if an embeddable, element collection or owned collection changes, that would trigger validation of the owning entity.
An entity is not affected by changes to associated entities or unowned collections, so that would still not trigger validation. If you have a @Version field, you could mark these associations with @OptimisticLock(excluded = false) to make sure that changes to them cause a version increment and hence mark the entity dirty, to trigger validation through that process.

Did a quick test with @OptimisticLock(excluded = false) in my first example. I added:

EntityA:

    @Version
    public Long version;

    @NotNull
    @OneToMany(mappedBy = "entityA", cascade = CascadeType.ALL)
    @OptimisticLock(excluded = false)
    public List<@Valid EntityB> entityB;

EntityB:

    @Version
    public Long version;

    @NotNull
    @ManyToOne
    @OptimisticLock(excluded = false)
    public EntityA entityA;

Test:

        entityA.field = "update";
        entityA = entityService.updateEntityA(entityA);              (1)

        assertEquals(1, entityA.version);
        assertEquals(0, entityB.version);                            (2)

        assertEquals("update", entityA.field);
        assertEquals("field-value", entityA.entityB.get(0).field);

Update does still no fail (no validation of EntityB) and entityB.version is not incremented.

My point is that if you have a EntityA with @Valid-annotated relations and EntityA gets validated with ORM, these relations will be ignored. So EntityA is by bean validation definition not valid.

In other words, there’s a common misconception that entities validated by the ORM are automatically valid according to the Bean Validation specification.

According to the JPA specification, this is by design.

Validation cascade (@Valid) must not occur for entity associations (single- or multi-valued).

Also, the JPA spec explains that validation callbacks happen through entity lifecycle callbacks, so if an entity is not updated, then the validation for that entity won’t happen. So if you want a change in an EntityB instance to trigger re-validation of EntityA, then you will have to update some timestamp in the EntityA instance to force an update.

Thanks for pointing that out — that’s what I was missing.