Announcement Announcement Module
Collapse
No announcement yet.
Spring TX interaction with caching / detached entities Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Spring TX interaction with caching / detached entities

    Hello,

    I'm using Spring Data JPA in a simple "frontend <=> service <=> repository" layered architecture.
    What I'd like to do is to make my @Service return "detached" entities to the frontend, so it can freely and securely manipulate these objects.
    That seems to be the default behavior "out of the box" (?) but that changes when I happen to use Spring transactions.
    Here's a simplified test case :


    Code:
    @Entity
    public class Record implements Serializable {
        @Id
        @GeneratedValue(generator = "uuid")
        private String id;
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        private String title;
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        private boolean published = false;
    
        public boolean isPublished() {
            return published;
        }
    
        public void setPublished(boolean published) {
            this.published = published;
        }
    }
    
    
    public interface RecordRepository
      extends JpaRepository<RecordRepository, String> {
    
    }
    
    @Service
    public class RecordService {
        @Resource
        private RecordRepository recordRepository;
    
        @Transactional
        public Record createRecord(Record recordRemote) {
            Record recordLocal = new Record();
            
            recordLocal.setTitle(recordRemote.getTitle());
            
            recordLocal = recordRepository.save(recordLocal);
    
            return recordLocal;
        }
        
        @Transactional
        public Record updateRecord(Record recordRemote) {
            Record recordLocal = recordRepository.findOne(recordRemote.getId());
    
            recordLocal.setTitle(recordRemote.getTitle());
           
            recordLocal = recordRepository.save(recordLocal);
    
            return recordLocal;
        }
    }
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "/beans.xml")
    @TestExecutionListeners(TransactionalTestExecutionListener.class)
    public class AppTest extends AbstractJUnit4SpringContextTests {
        @Resource
        private RecordService recordService;
    
        @Test
        @Transactional
        public void test1() {
            String TITLE = "my title";
    
            Record record = new Record();
            record.setTitle(TITLE);
    
            record = recordService.createRecord(record);
    
            assertEquals(TITLE, record.getTitle());
    
            TITLE = "another title";
    
            record.setTitle(TITLE);
            record.setPublished(true); // that shouldn't have any impact on the JPA entity (cache)
    
            record = recordService.updateRecord(record);
    
            assertEquals(TITLE, record.getTitle());
            assertFalse(record.isPublished()); // fails when using @Transactional
        }
    }
    Basically, the test succeeds... except when I try to use it in a @Transactional setup.
    So what's going on here ? Why does Spring TX have such an impact ?
    And, back to my initial goal, how can I easily returned detached entities out of my @Service methods?
    I suppose I could clone all my objects but I'd prefer to take advantage of JPA capabilities if possible...

    Environnement : Spring 3.0.7, Spring Data JPA 1.0.2, EclipseLink 2.2.1 with PostgreSQL.

    Thanks a lot!

  • #2
    Actually is behaves as I would expect it behaves... Your transaction spans your WHOLE test method.. So everything inside the method is happening INSIDE A SINGLE transaction... Your record object is reread not from the database but from the first level cache and guess what that is the SAME object as you created...

    Comment


    • #3
      Thanks for your help Marten!

      So, if I understand correctly, Spring TX maintains an additional cache when switched on (which is different from JPA cache presumably !? and how does it work with the SQL DB transactions ?).
      I've specified "@Transactional(propagation = Propagation.REQUIRES_NEW)" on my @Service and my test now succeeds.
      Is is the recommended solution for what I'm trying to achieve?

      In the end, will it produce what I'm aiming at: returning "detached" entities out of my @Service methods?

      Spring is a very nice tool as it handles many things almost automatically but that sometimes makes it difficult to understand what it actually does

      Regards.

      Comment


      • #4
        Spring doesn't maintain a cache your entity manager does and before it actually queries the database/loads an entity it first check its own first level cache for the existence of an entity with id x if it is there it will simply return the cached instance. Due to the fact that your whole test method is transactional the first level cache is never cleared/destroyed it is bound to the lifecycle of the entitymanager actually the entitymanager is the first level cache.

        So in short if basically fails due to your setup tohave your test method transactional, what you now have is a workaround which you shouldn't use!... To mimic the behavior what you can do is after the call to the service method call flush and then clear on the entitymanager (which you can simply inject into your testcase). But don't change your transaction settings due to the test not working to your expectations, understand what happens and adapt .

        And again it has nothing to do with spring it has all to do with transaction boundaries, the same would happen if you would create a service method which does what you want, make it transactional and call that from a non-transactional test case, the result would be the same, due to the transaction boundary.

        Comment


        • #5
          Indeed, my "@Transactional(propagation = Propagation.REQUIRES_NEW)" idea was clearly a workaround
          So, if I follow your advice, here what my @Service looks like now:

          Code:
          @Service
          public class RecordService {
              @Resource
              private RecordRepository recordRepository;
          
              @PersistenceContext
              private EntityManager entityManager;
          
              @Transactional
              public Record createRecord(Record recordRemote) {
                  Record recordLocal = new Record();
                  
                  recordLocal.setTitle(recordRemote.getTitle());
                  
                  recordLocal = recordRepository.saveAndFlush(recordLocal);
          
                  entityManager.detach(recordLocal);
          
                  return recordLocal;
              }
              
              @Transactional
              public Record updateRecord(Record recordRemote) {
                  Record recordLocal = recordRepository.findOne(recordRemote.getId());
          
                  recordLocal.setTitle(recordRemote.getTitle());
                 
                  recordLocal = recordRepository.saveAndFlush(recordLocal);
          
                  entityManager.detach(recordLocal);
          
                  return recordLocal;
              }
          }
          Is that the kind of solution you had in mind?
          As for my test, it succeeds with this setup.

          By the way, here's what the JPA doc says for EntityManager.detach():

          Remove the given entity from the persistence context, causing a managed entity to become detached. Unflushed changes made to the entity if any (including removal of the entity), will not be synchronized to the database.
          Entities which previously referenced the detached entity will continue to reference it.
          So I supposed that means that detached entities are not 100% detached, due to possible references?!
          Things are definitely not easy

          Regards.

          Comment


          • #6
            No that wasn't what i had in mind... I had in mind that you would inject the entitymanager into your unit test NOT your service... Your service should be as in the beginnining. You should simply call flush and clear after thes ervice method in your test method.

            Comment


            • #7
              Well, my test case should mimic what my client code will do in the final app.
              So throwing the EntityManager in there would not be realistic.

              Furthermore, I've looked at the implication of making use of EntityManager.detach(): many more SQL queries are made (i.e. to fetch again previously detached entities).
              The more I think about it, maybe the simplest way would be after all to simply clone the entity by hand, or using something like Apache Commons SerializationUtils.

              I've found a StackOverflow discussion on a subject close to mine: among EntityManager.clear() and EntityManager.detach(), some people recommends simply cloning the object.
              They also mention EclipseLink's JpaEntityManager.copy() method but I couldn't get it to work and I'm not fond of using such a specific API...

              Yet, I'm still surprised that I couldn't find what are the "best practices" in the Spring world to tackle this issue: to able to return objects from @Service methods that can be freely manipulated by client code, without any risk.

              Comment


              • #8
                Again it has NOTHING to do with Spring it has to do with transaction boundaries or to be more precise the lifecycle of the entitymanager!!!!! If you call from a transaction another transactional method that will not give you a detached entity (unless you start a new transaction which will lead to a new entitymanager or force to detach the entity).

                The code I mentioned IS mimicing the behavior of the client it is actually mimicing the transactional behavior of your normal flow (which is what you need if you make your testcase @Transactional).

                Comment


                • #9
                  OK, I think I see what you mean: my test case differs from my actual code because it is run transactionaly.
                  That's why you say that I need to adapt my test case in terms of clearing the cache. Am I right?

                  By the way, now what if I wanted to also run production code in transactions? Would that make sense?

                  Comment

                  Working...
                  X