Spring repository saves also objects that I'm not trying to save

KKaar :

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.

Klaus Groenbaek :

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.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=459720&siteId=1