Search code examples
javahibernatejpaspring-datadirty-checking

Spring data - entity not updated


I have an entity Customer and Spring data interface CustomerRepository shown below:

public interface CustomerRepository extends JpaRepository<Customer,Long> {
    Customer findCustomerByName(String name);
}

I save Customer object in the database and then update one field like this:

customerRepository.save(new Customer("John", "Smith"));
Customer john = customerRepository.findCustomerByName("John");
john.setSurname("Barton");
customerRepository.flush();
customerRepository.findAll().forEach(System.out::println);

I don't understand why it prints: Customer(id=1, name=John, surname=Smith).

As far as I know, Hibernate uses dirty checking mechanism to update entities in persistent state. So changed surname should be propagated to the database during end of transaction (but it does not - even if I separate this code into two @Transactional methods). Am I doing something wrong? Do I really need to save object manually after each change? Why surname field is not updated in the database?


Solution

  • @RunWith(SpringRunner.class)
    @SpringBootTest
    public class CustomerRepoTest {
        @Autowired
        private CustomerRepository customerRepository;
    
        @Test
        //NOTE: No @Transactional
        public void testSaveFails() throws Exception {
            customerRepository.save(new Customer("John", "Smith"));
            Customer john = customerRepository.findCustomerByName("John");
            john.setSurname("Barton");
            customerRepository.flush();
            customerRepository.findAll().forEach(System.out::println);
        }
    
        @Test
        @Transactional
        public void testSaveWorks() throws Exception {
            customerRepository.save(new Customer("John", "Smith"));
            Customer john = customerRepository.findCustomerByName("John");
            john.setSurname("Barton");
            //customerRepository.flush(); Flush is not necessary
            customerRepository.findAll().forEach(System.out::println);
        }
    
    }
    

    To explain: Hibernate keeps a cache of objects it loaded during a transaction. When a find method is executed, the id of a newly loaded object is compared to the id of the object in this cache, if found, the cached version is used.

    • This is why the version with @Transactional works.
    • Also, it explains why flush is not necessary - it only forces the values to be written before the transaction ends.

    If you miss the @Transactional (assuming auto-commit on the underlying transaction, which is most likely the case):

    • You save the entity in one transaction
    • Reload it with findCustomerByName, but it immediately gets detached
    • you modify the detached entity - no save
    • you reload the entries in another transaction, and you don't see your update