Trouble with hibernate optimistic lock and collections
I have a trouble in using optimistic locking. I have a persistent object, with a version number. I want this version number to be increased only when my persistent object is 'really' updated, meaning when one or more fields have been 开发者_开发技巧modified or when a collection mapped in my entity with a @ManyToOne
or @OneToMany
annotation is modified in the database.
What happens is that the version is increased only when a field contained directly in my entity has changed, and not when a collection has changed.
Note : I put select-before-update in my Entity annotation. Don't know if it might changed the behaviour of the versioning on the collections!
I also have a field which should not influence the version in my entity, on which I put the @OptimisticLock(exclude=true)
annotation.
Does anyone know how I may try to make my versioning work? According to what I've read on several forums, the version number should be automatically increased when a collection changed. Why wouldn't it be the case in my case? Any idea?
Only unidirectional collection changes are going to be propagated to the parent entity version. Because you are using a bidirectional association, it's the @ManyToOne
side that will control this association, so adding/removing an entity in the parent-side collection is not going to affect the parent entity version.
However, you can still propagate changes from child entities to parent entities. This requires you to propagate the OPTIMISTIC_FORCE_INCREMENT
lock whenever the child entity is modified.
In short, you need to have all your entities implementing a RootAware
interface:
public interface RootAware<T> {
T root();
}
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
private Long id;
private String title;
@Version
private int version;
//Getters and setters omitted for brevity
}
@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment
implements RootAware<Post> {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
private String review;
//Getters and setters omitted for brevity
@Override
public Post root() {
return post;
}
}
@Entity(name = "PostCommentDetails")
@Table(name = "post_comment_details")
public class PostCommentDetails
implements RootAware<Post> {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId
private PostComment comment;
private int votes;
//Getters and setters omitted for brevity
@Override
public Post root() {
return comment.getPost();
}
}
Then, you need two event listeners:
public static class RootAwareInsertEventListener
implements PersistEventListener {
private static final Logger LOGGER =
LoggerFactory.getLogger(RootAwareInsertEventListener.class);
public static final RootAwareInsertEventListener INSTANCE =
new RootAwareInsertEventListener();
@Override
public void onPersist(PersistEvent event) throws HibernateException {
final Object entity = event.getObject();
if(entity instanceof RootAware) {
RootAware rootAware = (RootAware) entity;
Object root = rootAware.root();
event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT);
LOGGER.info("Incrementing {} entity version because a {} child entity has been inserted", root, entity);
}
}
@Override
public void onPersist(PersistEvent event, Map createdAlready)
throws HibernateException {
onPersist(event);
}
}
and
public class RootAwareUpdateAndDeleteEventListener
implements FlushEntityEventListener {
private static final Logger LOGGER =
LoggerFactory.getLogger(RootAwareUpdateAndDeleteEventListener.class);
public static final RootAwareUpdateAndDeleteEventListener INSTANCE =
new RootAwareUpdateAndDeleteEventListener();
@Override
public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
final EntityEntry entry = event.getEntityEntry();
final Object entity = event.getEntity();
final boolean mightBeDirty = entry.requiresDirtyCheck( entity );
if(mightBeDirty && entity instanceof RootAware) {
RootAware rootAware = (RootAware) entity;
if(updated(event)) {
Object root = rootAware.root();
LOGGER.info("Incrementing {} entity version because a {} child entity has been updated",
root, entity);
incrementRootVersion(event, root);
}
else if (deleted(event)) {
Object root = rootAware.root();
LOGGER.info("Incrementing {} entity version because a {} child entity has been deleted",
root, entity);
incrementRootVersion(event, root);
}
}
}
private void incrementRootVersion(FlushEntityEvent event, Object root) {
event.getSession().lock(root, LockMode.OPTIMISTIC_FORCE_INCREMENT);
}
private boolean deleted(FlushEntityEvent event) {
return event.getEntityEntry().getStatus() == Status.DELETED;
}
private boolean updated(FlushEntityEvent event) {
final EntityEntry entry = event.getEntityEntry();
final Object entity = event.getEntity();
int[] dirtyProperties;
EntityPersister persister = entry.getPersister();
final Object[] values = event.getPropertyValues();
SessionImplementor session = event.getSession();
if ( event.hasDatabaseSnapshot() ) {
dirtyProperties = persister.findModified(
event.getDatabaseSnapshot(), values, entity, session
);
}
else {
dirtyProperties = persister.findDirty(
values, entry.getLoadedState(), entity, session
);
}
return dirtyProperties != null;
}
}
which you can register as follows:
public class RootAwareEventListenerIntegrator
implements org.hibernate.integrator.spi.Integrator {
public static final RootAwareEventListenerIntegrator INSTANCE =
new RootAwareEventListenerIntegrator();
@Override
public void integrate(
Metadata metadata,
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
final EventListenerRegistry eventListenerRegistry =
serviceRegistry.getService( EventListenerRegistry.class );
eventListenerRegistry.appendListeners(EventType.PERSIST, RootAwareInsertEventListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.FLUSH_ENTITY, RootAwareUpdateAndDeleteEventListener.INSTANCE);
}
@Override
public void disintegrate(
SessionFactoryImplementor sessionFactory,
SessionFactoryServiceRegistry serviceRegistry) {
//Do nothing
}
}
and then supply the RootAwareFlushEntityEventListenerIntegrator
via a Hibernate configuration property:
configuration.put(
"hibernate.integrator_provider",
(IntegratorProvider) () -> Collections.singletonList(
RootAwareEventListenerIntegrator.INSTANCE
)
);
Now, when you modify a PostCommentDetails
entity:
PostCommentDetails postCommentDetails = entityManager.createQuery(
"select pcd " +
"from PostCommentDetails pcd " +
"join fetch pcd.comment pc " +
"join fetch pc.post p " +
"where pcd.id = :id", PostCommentDetails.class)
.setParameter("id", 2L)
.getSingleResult();
postCommentDetails.setVotes(15);
The parent Post
entity version is modified as well:
SELECT pcd.comment_id AS comment_2_2_0_ ,
pc.id AS id1_1_1_ ,
p.id AS id1_0_2_ ,
pcd.votes AS votes1_2_0_ ,
pc.post_id AS post_id3_1_1_ ,
pc.review AS review2_1_1_ ,
p.title AS title2_0_2_ ,
p.version AS version3_0_2_
FROM post_comment_details pcd
INNER JOIN post_comment pc ON pcd.comment_id = pc.id
INNER JOIN post p ON pc.post_id = p.id
WHERE pcd.comment_id = 2
UPDATE post_comment_details
SET votes = 15
WHERE comment_id = 2
UPDATE post
SET version = 1
where id = 1 AND version = 0
The same goes for the PostComment
entity as well.
And it works even if you insert a new child entity:
Post post = entityManager.getReference(Post.class, 1L);
PostComment postComment = new PostComment();
postComment.setId(3L);
postComment.setReview("Worth it!");
postComment.setPost(post);
entityManager.persist(postComment);
Hibernate managing to increment the parent entity properly:
SELECT p.id AS id1_0_0_ ,
p.title AS title2_0_0_ ,
p.version AS version3_0_0_
FROM post p
WHERE p.id = 1
INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Worth it!', 3)
UPDATE post
SET version = 3
WHERE id = 1 AND version = 2
It also works when removing child entities:
PostComment postComment = entityManager.getReference(PostComment.class, 3l);
entityManager.remove(postComment);
Hibernate managing to increment the parent entity in this use case as well:
SELECT pc.id AS id1_1_0_ ,
pc.post_id AS post_id3_1_0_ ,
pc.review AS review2_1_0_
FROM post_comment pc
WHERE pc.id = 3
SELECT p.id AS id1_0_0_ ,
p.title AS title2_0_0_ ,
p.version AS version3_0_0_
FROM post p
WHERE p.id = 1
DELETE FROM post_comment
WHERE id = 3
UPDATE post
SET version = 4
WHERE id = 1 and version = 3
I thought, after reading documentation as this : https://docs.redhat.com/docs/en-US/JBoss_Enterprise_Web_Server/1.0/html/Hibernate_Annotations_Reference_Guide/ch03s04s03s08.html, that @OptimisticLock(excluded = true)
prevent the version to be incremented. So in the example you give, if you put it on the field mapping the collection it would not act correctly.
I think the good exemple should be :
@OneToMany(mappedBy = "parent", cascade = {CascadeType.ALL})
public Set<Child> getChildren()
{
return children;
}
or
@OneToMany(mappedBy = "parent", cascade = {CascadeType.ALL})
@OptimisticLock(excluded = false)
public Set<Child> getChildren()
{
return children;
}
Select before update only retrieves the state of the entity instance and not the collections and associations. So when you update the collection, and then update the entity, the select before update ignores the collection update. You can try to use merge operation which loads the entity (including collections) and copies the changes from detached instance and hands you the entity which is persistent.
You should add some parameters (CascadeType) to @OneToMany annotation, e.g. (parent entity)
@OneToMany(mappedBy = "parent", cascade = {CascadeType.ALL})
@OptimisticLock(excluded = true)
public Set<Child> getChildren()
{
return children;
}
And in @ManyToOne (child entity)
@ManyToOne
@OptimisticLock(excluded = true)
Example http://shrubbery.homeip.net/c/display/W/Hibernate+JPA+Tips (domain name changed)
精彩评论