【问题标题】:Spring repository saves also objects that I'm not trying to saveSpring 存储库还保存了我不想保存的对象
【发布时间】:2018-08-16 14:56:33
【问题描述】:

问题是有一天我们发现如果我们将一个对象保存在 Spring Boot 存储库中,另一个以相同方法更改的对象也会更新并持久化到数据库中。

人们非常好奇为什么会发生这种情况。我使用Spring Initializr 和一些模板代码创建了示例项目来展示实际情况(尽量保持依赖的数量尽可能少)。

使用 Spring boot 版本 1.5.11 (SNAPSHOT) 并且项目具有以下依赖项:

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')
}

现在进入重点:

项目有两个实体,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; }
}

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; }
}

这两个实体也都有存储库,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);
}

还有一个真正发生魔法的简单服务(我预先保存了一个 Pet 和一个 User 对象,具有不同的名称和类型)

@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;
  }

}

在调用userRepository.save(user); 之后,Pet 对象也在数据库中更新为新类型的“Cow”。如果我只保存 User 对象,为什么会发生这种情况?这是故意的吗?

还有一个简单的控制器和简单的测试端点来调用服务方法,这很可能对问题并不重要,但为了完整起见,我还是在这里添加它。

@RestController
public class UserController {

  @Autowired
  UserService userService;

  @RequestMapping(value = "/test", method = RequestMethod.GET)
  public User changeUserAndPet() {
    return userService.changeUserAndPet();
  }

}

感谢任何解释/提示,并随时在 github 中询问更多信息/代码。

【问题讨论】:

  • 启用 jpa 日志并查看执行的查询。
  • 我知道查询是针对两个更新执行的,我只是想知道是什么触发了Pet对象的更新查询:)

标签: java spring spring-mvc jpa spring-boot


【解决方案1】:

Spring Data 存储库是 JPA EntityManager 的包装器。加载实体时,您会获得实例,但对象的副本存储在EntityManager 中。当您的事务提交时,EntityManager 迭代所有托管实体,并将它们与它返回给您的代码的版本进行比较。如果您对版本进行了任何更改,JPA 会计算应在数据库中执行哪些更新以反映您的更改。

除非您非常了解 JPA,否则很难预测调用何时传播到数据库,因为 flush() 是在内部调用的。例如,每次执行查询时,JPA 都会执行查询前刷新,因为任何待处理的插入都必须发送到数据库,否则查询将找不到它们。

如果你在你的方法上使用@Transactional 定义了一个事务,那么即使用户没有被保存,pet 也会被更新。当您没有事务时,对 save 的调用必须触发 EntityManager 以将您的更新传播到数据库。为什么会发生这种情况对我来说有点神秘。我知道 Spring 在调用 Controller 之前会在 OpenEntityManagerInViewInterceptor 内部创建 EntityManager,但由于事务不是显式的,因此必须隐式创建它,并且可能存在多个事务。

我总是鼓励开发人员在 Spring 中使用显式事务,并在适当的时候使用只读来限定它们。

【讨论】:

  • 哇,你说得对。每当在 (@Transactional) 结束时到达 em.commit() 时,即使没有保存部分,该方法中的所有内容都会被刷新。
  • 我读到的关键内容,在什么事情更有意义之后:1)发布于:vladmihalcea.com/…,了解 JPA 的默认刷新模式如何工作以及何时调用内部刷新 2)JPA 实体生命周期如何工作来自 dunni 和 3)@saljuama 的答案stackoverflow.com/questions/35817584/… 的答案,当实际调用持续和合并时。还有你们提供的答案,使谷歌搜索到正确的东西成为可能。
【解决方案2】:

这就是 JPA 和 EntityManager 的工作方式。如果您通过存储库查找实体,它将作为托管实体附加到 EntityManager。当 EntityManager 执行刷新时,您对该对象所做的任何更改都会被拾取。事实上,在您的情况下,您甚至不需要调用存储库上的 save 方法。

您可以找到有关 JPA 实体生命周期的更多信息,例如这里:https://dzone.com/articles/jpa-entity-lifecycle

【讨论】:

  • 当我注释掉存储库保存方法时实际上没有发生任何事情,实体没有更新
  • 啊,那可能是因为你没有用@Transactional注释服务方法。这就是为什么需要保存才能触发刷新。
  • 是的,你是对的。从@Klaus Groenbaek 的回答中也发现了这一点。使用@Transactional 它可以工作,因为flush() 在方法结束时被触发。感谢您的努力。
猜你喜欢
  • 2019-09-25
  • 2011-12-29
  • 1970-01-01
  • 2019-11-07
  • 1970-01-01
  • 1970-01-01
  • 2018-06-15
  • 1970-01-01
  • 2013-10-23
相关资源
最近更新 更多