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);
}
}
To resolve this error we have two ways: Get the latest object from the database and set the old object values if you need those values to be persisted to the new object and merge it. For the old object set the latest version from Database.
In order to use optimistic locking, we need to have an entity including a property with @Version annotation. While using it, each transaction that reads data holds the value of the version property. Before the transaction wants to make an update, it checks the version property again.
There are two models for locking data in a database: Optimistic locking , where a record is locked only when changes are committed to the database. Pessimistic locking , where a record is locked while it is edited.
Optimistic locking ssumes that multiple transactions can complete without affecting each other, and that therefore transactions can proceed without locking the data resources that they affect. Before committing, each transaction verifies that no other transaction has modified its data.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With