10

Let's suppose to have this situation:

We have Spring Data configured in the standard way, there is a Respository object, an Entity object and all works well.

Now for some complex motivations I have to use EntityManager (or JdbcTemplate, whatever is at a lower level than Spring Data) directly to update the table associated to my Entity, with a native SQL query. So, I'm not using Entity object, but simply doing a database update manually on the table I use as entity (it's more correct to say the table from which I get values, see next rows).

The reason is that I had to bind my spring-data Entity to a MySQL view that makes UNION of multiple tables, not directly to the table I need to update.

What happens is:

In a functional test, I call the "manual" update method (on table from which the MySQL view is created) as previously described (through entity-manager) and if I make a simple Respository.findOne(objectId), I get the old object (not updated one). I have to call Entitymanager.refresh(object) to get the updated object.

Why?

Is there a way to "synchronize" (out of the box) objects (or force some refresh) in spring-data? Or am I asking for a miracle? I'm not ironical, but maybe I'm not so expert, maybe (or probably) is my ignorance. If so please explain me why and (if you want) share some advanced knowledge about this amazing framework.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
andPat
  • 4,153
  • 7
  • 24
  • 36
  • Are you performing the update and fetch within the scope of the same test? – manish Nov 20 '15 at 11:51
  • Yes within the same test. – andPat Nov 20 '15 at 12:38
  • 1
    Then that (object not being refreshed on calling `findOne`) is expected behaviour. Assuming that you are using Hibernate as the JPA provider (the logic is the same for all JPA providers), the same Hibernate `Session` is in use during a single test and Hibernate `Session` cache objects. `EntityManager.refresh` forces Hibernate to ignore the cache content and load data from the database afresh. – manish Nov 20 '15 at 12:42
  • And what happens at runtime in a real environment (deployed application)? I guess there's a cache also in that case. How can I (eventually) disable it? – andPat Nov 20 '15 at 12:45
  • 3
    If your actual code is `@Transactional ... update(...)`, followed by `Entity fetch(...)`, then you will be fine because `update` (where you actually perform the update) and `fetch` (where you fetch the updated data) are within two different transaction boundaries and therefore will not share the same `Session`, even if they are called as `update(...); Entity entity = fetch(...);`. If they are within the same transaction boundary, then they will share the same `Session` and you will have a stale object. – manish Nov 20 '15 at 12:50
  • Now it's a bit clearer... thank you very much! – andPat Nov 20 '15 at 13:28
  • So if I have `@Transactional` on the class DAO that implements `update` and `fetch` I guess that I'm on the same transaction and I'm subject to this cache problem, or not? – andPat Nov 20 '15 at 15:41
  • Hard to say without looking at your full actual code as transaction management is a tricky thing. – manish Nov 20 '15 at 15:49
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/95726/discussion-between-andpat-and-manish). – andPat Nov 20 '15 at 16:27

3 Answers3

7

If I make a simple Respository.findOne(objectId) I get old object (not updated one). I've to call Entitymanager.refresh(object) to get updated object.

Why?

The first-level cache is active for the duration of a session. Any object entity previously retrieved in the context of a session will be retrieved from the first-level cache unless there is reason to go back to the database.

Is there a reason to go back to the database after your SQL update? Well, as the book Pro JPA 2 notes (p199) regarding bulk update statements (either via JPQL or SQL):

The first issue for developers to consider when using these [bulk update] statements is that the persistence context is not updated to reflect the results of the operation. Bulk operations are issued as SQL against the database, bypassing the in-memory structures of the persistence context.

which is what you are seeing. That is why you need to call refresh to force the entity to be reloaded from the database as the persistence context is not aware of any potential modifications.

The book also notes the following about using Native SQL statements (rather than JPQL bulk update):

■ CAUTION Native SQL update and delete operations should not be executed on tables mapped by an entity. The JP QL operations tell the provider what cached entity state must be invalidated in order to remain consistent with the database. Native SQL operations bypass such checks and can quickly lead to situations where the inmemory cache is out of date with respect to the database.

Essentially then, should you have a 2nd level cache configured then updating any entity currently in the cache via a native SQL statement is likely to result in stale data in the cache.

Alan Hay
  • 22,665
  • 4
  • 56
  • 110
1

In Spring Boot JpaRepository:

If our modifying query changes entities contained in the persistence context, then this context becomes outdated.

In order to fetch the entities from the database with latest record.

Use @Modifying(clearAutomatically = true)

@Modifying annotation has clearAutomatically attribute which defines whether it should clear the underlying persistence context after executing the modifying query.

Example:

@Modifying(clearAutomatically = true)
        @Query("UPDATE NetworkEntity n SET n.network_status = :network_status WHERE n.network_id = :network_id")
    int expireNetwork(@Param("network_id") Integer network_id,  @Param("network_status") String network_status);
Narasimha A
  • 319
  • 5
  • 14
0

Based on the way you described your usage, fetching from the repo should retrieve the updated object without the need to refresh the object as long as the method which used the entity manager to merge has @transactional

here's a sample test

@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ApplicationConfig.class)
@EnableJpaRepositories(basePackages = "com.foo")
public class SampleSegmentTest {

    @Resource
    SampleJpaRepository segmentJpaRepository;

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    @Test
    public void test() {
        Segment segment = new Segment();
        ReflectionTestUtils.setField(segment, "value", "foo");
        ReflectionTestUtils.setField(segment, "description", "bar");

        segmentJpaRepository.save(segment);

        assertNotNull(segment.getId());
        assertEquals("foo", segment.getValue());
        assertEquals("bar",segment.getDescription());

        ReflectionTestUtils.setField(segment, "value", "foo2");
        entityManager.merge(segment);

        Segment updatedSegment = segmentJpaRepository.findOne(segment.getId());
        assertEquals("foo2", updatedSegment.getValue());
    }

}
geneqew
  • 2,401
  • 5
  • 33
  • 48
  • Thank you for the response! Perfect, this is my situation! BUT there is a litlle (or big) difference. Instead of `entityManager.merge(segment);` I do `entityManager.createNativeQuery( "UPDATE segment_table set value = 'foo2' WHERE segment_id = 1" ).executeUpdate();` I think that this could be the problem. but I can't change query. – andPat Nov 20 '15 at 12:42