How to handle JPA ObjectOptimisticLockException properly for multiple simultaneous transaction requests?

adn.911 picture adn.911 · Apr 11, 2015 · Viewed 20.6k times · Source

So, I was working on a simple Spring MVC + JPA (hibernate) project where there are Users who can makes Posts and make Comments on their friends Posts (somewhat like a small social network) . I am still relatively new using JPA Hibernate. So, when I try to test from browser sending multiple requests for some task ( containing transactions) very quickly 2-3 times while a previous request is being processed I get an OptimisticLockException . Here 's the stack trace ..

org.springframework.web.util.NestedServletException: Request processing   failed; nested exception is org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [org.facebookjpa.persistance.entity.Post] with identifier [19]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.facebookjpa.persistance.entity.Post#19]
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:973)
org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)
javax.servlet.http.HttpServlet.service(HttpServlet.java:620)
org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
javax.servlet.http.HttpServlet.service(HttpServlet.java:727)

Now, how do i fix this? How do i handle this ObjectOptimisticLockException properly when multiple transaction requests occurs simultaneously ? Is there any good patten that i should follow ? Do i need to use some sort of Pessimistic Locking mechanism ?

Here's the DAO that i am currently using .. Thanks in advance . :)

@Repository
@Transactional
public class PostDAOImpl implements PostDAO {

@Autowired
UserDAO userDAO;

@Autowired
CommentDAO commentDAO;

@Autowired
LikeDAO likeDAO;

@PersistenceContext
private EntityManager entityManager;

public PostDAOImpl() {

}

@Override
public boolean insertPost(Post post) {
    entityManager.persist(post);
    return true;
}

@Override
public boolean updatePost(Post post) {
    entityManager.merge(post);
    return true;
}

@Override
public Post getPost(int postId) {
    TypedQuery<Post> query = entityManager.createQuery("SELECT p FROM Post AS p WHERE p.id=:postId", Post.class);
    query.setParameter("postId", postId);
    return getSingleResultOrNull(query);
}

@Override
public List<Post> getAllPosts() {

    return entityManager.createQuery("SELECT p FROM Post AS p ORDER BY p.created DESC", Post.class).getResultList();
}

@Override
  public List<Post> getNewsFeedPostsWithComments(int userId) {
    List<Post> newsFeedPosts = getUserPosts(userId);
    newsFeedPosts.addAll(getFriendsPost(userDAO.getUser(userId)));

    for (Post post : newsFeedPosts) {
        post.setComments(commentDAO.getPostComments(post.getId()));
        post.setLikes(likeDAO.getPostLikes(post.getId()));
    }

    return newsFeedPosts;
}

public List<Post> getFriendsPost(User user) {
    List<Post> friendsPosts = new ArrayList<Post>();

    for (User u : user.getFriends()) {
        friendsPosts.addAll(getUserPosts(u.getId()));
    }

    return friendsPosts;
}


@Override
public List<Post> getUserPosts(int userId) {
    TypedQuery<Post> query = entityManager.createQuery("SELECT p FROM Post AS p WHERE p.user.id = :userId ORDER BY p.created DESC", Post.class);
    query.setParameter("userId", userId);
    return query.getResultList();
}

@Override
public List<Post> getUserPostsWithComments(int userId) {
    List<Post> userPostsWithComments = getUserPosts(userId);

    for (Post post : userPostsWithComments) {
        post.setComments(commentDAO.getPostComments(post.getId()));
        post.setLikes(likeDAO.getPostLikes(post.getId()));
    }

    return userPostsWithComments;
}

@Override
public boolean removePost(Post post) {
    entityManager.remove(post);
    return true;
}

@Override
public boolean removePost(int postId) {
    entityManager.remove(getPost(postId));
    return true;
}


private Post getSingleResultOrNull(TypedQuery<Post> query) {
    query.setMaxResults(1);
    List<Post> list = query.getResultList();
    if (list.isEmpty()) {
        return null;
    }
    return list.get(0);
}

}

Answer

Vlad Mihalcea picture Vlad Mihalcea · Apr 11, 2015

The JPA OptimisticLockException prevents lost updates, and you shouldn't ignore it.

You can simply catch it in a common exception handler and redirect the user to the starting point of the currently executing workflow, indicating that the flow has to be restarted since it was operating with stale data.

However, if your application requirements don't need to prevent the lost update anomaly, then you can simply remove the @Version annotation from your entities and, therefore, break Serializability.

Now, you might think that an auto-retry against a fresh entity database snapshot will fix the problem, but you will end up with the same optimistic locking exception since the load-time version is still lower than the current entity version in the DB.

Also, you can use pessimistic locking (e.g. PESSIMISTIC_WRITE or PESSIMISTIC_READ) so that, once a row-level lock is acquired, no other transaction can modify the locked record.