2

I'm using spring-boot 1.5.4 with hibernate-core 5.2.10.

I have a controller that invokes a service method (lets name it pService) to save an entity after some logic

pService.save is something like this:

@Transactional(readOnly = false, rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public <S extends SomeEntity> S save(S entity) throws ServiceException {
    //some logic

    entity.setAttrX("original text");
    S justSavedEntity = someEntityRepository.save(entity); // POINT 1

   //we termporarily change one attribute just to get a legible representation calling another service method (lets name it eService)
   eService.process(justSavedEntity);

   return justSavedEntity; //POINT 2

}

eService.process is something like this:

@Transactional(readOnly = false, propagation = Propagation.MANDATORY)
public void process(SomeEntity entity) {

    //some logic
    entity.setAttrX("someText");
    //method ends here
}

Because there's no other call to save apart of the one at POINT 1, I expect the text "original text" to be saved in database, but the text I'm getting saved is the text changed at POINT 2.

So, why I'm getting saved "someText" instead of "original text"?

I can see in the database log that after POINT 1 an SQL INSERT command is runned and when pService returns, and unwanted SQL UPDATE command is runned changing "original text" to "someText" causing the error.

why is this "extra" UPDATE command issued?

Thanks in advance!

Neil Stockton
  • 11,383
  • 3
  • 34
  • 29
Dreal
  • 162
  • 2
  • 8

1 Answers1

4

This is the expected behaviour as described by the JPA specification. You changed a managed entity, which is intended logically to be the same thing as changing the stored data, so before the transaction completes the state of the entity is synced with the database. The idea is that you don't have to call save at the end of the method -- it's redundant.

Arnold Galovics has written a pretty good blog post about this with pretty diagrams.

Given this it becomes obvious that you shouldn't be mutating the entity this way. It suggests that the 'process' you're calling isn't treating the object it is passed as an entity. It's a common mistake to think that just because your current object seems to provide everything you need in a different context that it's a good idea to use it in that context, rather than checking that the object represents the same concept. In this case, your process doesn't want an entity.

You should be creating a value object of some kind to pass to the process method. Creating a value object could be as easy as detaching the entity from it's persistence context: Spring JpaRepository - Detach and Attach entity

Or, you could just calculate the value of the field and pass it as a second param to your process method on eService. Your final option is to send the entity as is and have the receiving service calculate the 'legible' representation.

However, you have other problems. You'd probably realise what they are if you transitioned to spring-data-jpa rather than writing your own repository classes. In simple terms, the repository class is intended to store and retrieve an entity in a persistent store of some kind, it is not intended to invoke other services. That would be the responsibility of whatever code was using the repo in the first place. In a spring-mvc app this would typically be a controller, which exists to orchestrate your applications services.

To put it plainly, you shouldn't be calling a service from within a repo class. It should do nothing but CRUD on an entity -- absolutely nothing else, ever. If you need it to do something else, then you've probably done something else wrong or you've picked the wrong architecture for your app.

You should take a look at spring-data-jpa. It's configured by default to auto-generate the repo's implementation at run time. This would probably reduce your code base, save you a load of time and defects, and would not easily allow you to make this class of architectural mistake. And, to top it off, it's a lot easier than what you're doing now.

ljgw
  • 2,751
  • 1
  • 20
  • 39
Software Engineer
  • 15,457
  • 7
  • 74
  • 102
  • thanks a lot for your answer. This is weird, because we only see this behavior after upgrade to spring-boot 1.5, 1.1 and 1.2 required a repository.save invocation to persist an entity. Any ideas what changed? – Dreal Jul 07 '17 at 16:04
  • There are too many possible explanations for what you have seen so any response to your question would be purely speculative. But you should probably be aware that this behaviour is part of the core JPA specification and has always been implemented this way in spring's jpa subsystems. – Software Engineer Jul 07 '17 at 23:52
  • Also, I've edited the answer to add something pertinent at the end. – Software Engineer Jul 08 '17 at 00:05
  • And, feel free to mark the answer as correct any time ;) – Software Engineer Jul 08 '17 at 00:09