The problem is that one day we discovered that if we're saving an object in spring boot repository, another objects that are changed in the same method are also updated and persisted in the database.
The curiosity is massive to find out why does this actually happen. I created sample project using Spring Initializr and some template code to show the actual situation (tried to keep the number of dependencies as low as possible).
Using Spring boot version 1.5.11 (SNAPSHOT) and project has following dependencies:
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.mariadb.jdbc:mariadb-java-client:2.1.0')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Now to the point:
Project has two entities, Pet
:
@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Pet.class)
public class Pet {
@Id
@GeneratedValue
private long id;
private String type;
public Pet() {}
public String getType() { return type; }
public void setType(String type) { this.type = type; }
}
and User
:
@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = User.class)
public class User {
@Id
@GeneratedValue
private long id;
private String name;
public User() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
Both entities also have repositories, Pet
:
@Repository
public interface PetRepository extends CrudRepository<Pet, Long> {
Pet findPetById(Long id);
}
User
:
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
User findUserById(Long id);
}
And one simple service where the magic actually happens ( I have pre-saved one Pet
and one User
object, with different name and type)
@Service
public class UserService {
@Autowired
UserRepository userRepository;
@Autowired
PetRepository petRepository;
public User changeUserAndPet() {
User user = userRepository.findUserById(1L);
Pet pet = petRepository.findPetById(1L);
user.setName("Kevin");
pet.setType("Cow");
userRepository.save(user);
return user;
}
}
Right after calling userRepository.save(user);
the Pet object is also updated in the database with new type of 'Cow'. Why exactly does this happen if I only saved the User
object? Is this intended to be like this?
There's also one simple controller and simple test endpoint to call the service method which most likely is not important to the question, but I'll still add it here for the sake of completeness.
@RestController
public class UserController {
@Autowired
UserService userService;
@RequestMapping(value = "/test", method = RequestMethod.GET)
public User changeUserAndPet() {
return userService.changeUserAndPet();
}
}
Any explanation / tips are appreciated and feel free to ask extra information / code in github.
The Spring Data repository is a wrapper around the JPA EntityManager
. When an entity is loaded, you get the instance, but a copy of the object is stored inside the EntityManager
. When your transaction commits, the EntityManager
iterates all managed entities, and compares them to the version it returned to your code. If you have made any changes to your version, JPA calculates which updates should be performed in the database to reflect your changes.
Unless you know JPA quite well, it can be tricky to predict when calls are propagated to the database, since flush()
is called internally. For instance every time you do a query JPA performs a pre-query flush, because any pending inserts must be send to the database, or the query would not find them.
If you defined a transaction using @Transactional
on you method, then pet
would be updated even if the user was not saved. When you don't have a transaction, the call to save must trigger the EntityManager
to propagate your update to the database. It's a bit of a mystery to me why this happens. I Know that Spring creates the EntityManager
inside OpenEntityManagerInViewInterceptor
before the Controller is called, but since the transaction is not explicit, it must be created implicitly and there could potentially be multiple transactions.
I always encourage developers to use explicit transactions in Spring, and qualify them with readonly when appropriate.
That's how JPA and the EntityManager works. If you lookup an entity through the repository, it is attached to the EntityManager as managed entity. Any changes that you do to that object, are picked up when a flush is executed by the EntityManager. In fact, you wouldn't even need to call the save method on the repository in your case.
You can find more information about the lifecycle of JPA entities e.g. here: https://dzone.com/articles/jpa-entity-lifecycle
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With