Before we start, if you are not familiar with the states you can first read the related post.
Let's assume that you are using a basic entity
@Entity
@Table(name = product)
public class Product {
@Id
private int id;
private int stock;
}
and the scenario you will apply is to decrease the number of stock of your product. You came up with an implementation design that you are going to first find your product and decrease by 1. Finally, you will check the count of product if you are successful.
@Test
void testDecreaseStock() {
// Let's say the product id is 1
Product oldProduct = productService.find(1);
oldProduct.decrementStock(oldProduct);
Product newProduct = productService.find(1);
Logger.info("Product stock is: ", newProduct.getStock() )
}
and the details of your product service is below
@Transactional
public Product find(int id) {
return entityManager.find(Product.class, id);
}
@Transactional
public void decrementStock (Product p) {
p.setStock(p.getStock() - 1);
entityManager.merge(p);
}
Hope this is not confused your mind since these are the very basic implementation of find() and decrementStock() methods. So, everything is clear and you ran your code. However, you have noticed that 4 states ran instead of 3. How?
Expected:
Product oldProduct = productService.find(1);
1
oldProduct.decrementStock(oldProduct);
2
Product newProduct = productService.find(1);
3
Actual:
select ... from product where id=1
1
select ... from product where id=1
2
update product set stock where id=1
3
select ... from product where id=1
4
Why we have the extra select query in the second place?
Whatever we do (insert, update, delete), Hibernate wants to figure it out first. The mechanism of dirty check. It will load the snapshot of the database and basically executes the select statement in order to figure out "What we have in the database now?" Did you notice the extra select query? That's why we have the line 2 on the output.
Let's fix it ✔️
The solution would be passing the id of the product to decrementStock() and remove the merge() since we are already in the "Managed" phase. Our transaction continues in the decrementStock() method. But when we have found the product and passed it to the decrementStock(), our transaction ended already and the state changed to "Detached". Anything you want to change in "Detached" state is INVALID. Hibernate can't figure out what you have done there.
@Test
void testDecreaseStock() {
// Let's say the product id is 1
oldProduct.decrementStock(1);
Product newProduct = productService.find(1);
Logger.info("Product stock is: ", newProduct.getStock() )
}
and the details of your product service is below
@Transactional
public Product find(int id) {
return entityManager.find(Product.class, id);
}
@Transactional
public void decrementStock (int id) {
Product p = entityManager.find(Product.class, id);
p.setStock(p.getStock() - 1);
}
What was the problem? 🤔
Since we were out of transaction scope when we first find the product and then continue with the other transactions, we were trying to change the product in "Detached" state and we have used merge() to change the state to "Managed" imperatively. Because we know that we should be in the "Managed" state. Here is the scope that we are trying to change our product.
The first code:
find(1) -> State: Managed -> product found -> Detached
find(1) -> State: Managed -> product found -> Detached
merge() -> State: Detached -> Managed -> stock updated -> Detached
find(1) -> State: Managed -> product found -> Detached
The fix code:
find(1) -> State: Managed -> product found -> Detached
merge() -> State: Detached -> Managed -> stock updated -> Detached
find(1) -> State: Managed -> product found -> Detached
Top comments (0)