简体   繁体   中英

Bean validation @ElementCollection and @Version conflict and fails validation

I am facing a very strange issue at the moment.

I have an entity that contains a property that is an element collection.

@ElementCollection(targetClass=Integer.class, fetch = FetchType.EAGER)
@CollectionTable(name="campaign_publisher", joinColumns=@JoinColumn(name="campaign_id"))
@Column(name = "publisher_id")

...

@NotEmpty(message = "campaign.publishers.missing")
public Set<Integer> getPublishers() {
    return this.publishers;
}

public Campaign setPublishers(Set<Integer> publisherId) {
    this.publishers = publisherId;
    return this;
}

This all works fine. The values are validated and saved correct.

I also want this entity to have optimistic concurrency so I applied a @Version annotation as well.

@Version
private Long etag = 0L;

...

public Long getEtag() {
    return etag;
}

public void setEtag(Long etag) {
    this.etag = etag;
}

By adding the @Version annotation the @NotEmpty validation on my set of publishers always returns invalid.

To try and diagnose this I have tried the following:

  • Creating a custom validator at the entity level so I can inspect the values in the entity. I found that the Set of values have been replaced with an empty PersistentSet which is causing the validation to always fail.

  • I created some unit tests for the entity that uses a validator that is retrieved from the validationfactory and this validator seems to work as expected.

  • I have also tried to change the ElementCollection to a many-to-many relationship and a bi-directional one-to-many but the issue persists.

Right now I am out of ideas. The only thing I have found that works correctly is disabling the hibernate validation and manually calling the validator just before I save my data.

So my questions are:

  • Has anyone encountered this issue before?
  • Any advice on what I could try next?

Thank you all for reading!

Short answer: Set the initial value for etag = null .

// this should do the trick
@Version
private Long etag = null;

Longer one : When you are adding a optimistic locking via adding @Version annotation on a field with a default value you are making hibernate/spring-data think that the entity is not a new one (even the id is null ). So on initial save instead of persisting entity undelying libraries try to do a merge. And merging transient entity forces hibernate to just one by one copy all the properties from source entity (the ones which you are persisting) to the target one (which is autocreate by hibernate with all the properties set to default values aka nulls) and here comes the problem, as hibernate will just copy the values of associations of FROM_PARENT type or in other words only associations which are hold on entity side but in your case the association is TO_PARENT (a foreign key from child to parent) hibernate will try to postpone association persistance after main entity save, but save will not work as entity will not pass @NotEmpty validation.

First I would suggest to remove the default value initialization for your @Version property. This property is maintained by hibernate, and should be initialized by it.

Second: are you sure that you are validating the fully constructed entity? ie you are constructing something, then do something, and for exact persist/flush cycle your entity is in wrong condition.

To clarify this, while you are on a Spring side, I would suggest to introduce service-level validation on your DAO layer. Ie force the bean validation during initial call to DAO, rather then bean validation of entity during flush (yeap hibernate batches lots of things, and exact validation happens only during flush cycle).

To achieve this: mark your DAO @Validated and make your function arguments beign validated: FancyEntity store(@Valid @NotNull FancyEntity fancyEntity) { fancyEntity = em.persist(fancyEntity); em.flush(); return fancyEntity;} FancyEntity store(@Valid @NotNull FancyEntity fancyEntity) { fancyEntity = em.persist(fancyEntity); em.flush(); return fancyEntity;}

By making this, you will be sure that you are storing valid entity: the validation would happen before store method is called. This will reveal the place where your entity became invalid: in your service layer, or in bad behaving hibernate layer.

I noticed that you use mixed access: methods and fields. In this case you can try to set @Version on the method:

@Version
public Long getEtag() {
    return etag;
}

not on the field.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM