【问题标题】:Properly remove entity正确删除实体
【发布时间】:2018-02-01 15:19:08
【问题描述】:

关于 JPA 的工作方式,我认为以下是正确的:将相关实体删除或添加到另一个实体(例如,Employee 到 Department.employees)时,关系的双方都必须相应地更新。也就是说,在删除 Employee 的特定实例中,我不应该只是从 EntityManager 中删除它,还要从引用 Department 对象中删除它。不这样做可能会导致其他操作无法正常工作,例如在 Spring Data JPA 示例中:

@Transactional
   public void test() {
       Function fn = repFun.findOne(75L);
       //at this point fn.tests = { Test{76} }
       doSomeDelegation(fn.getTests().iterator().next()); 
       fn.setName("new name");
       repFun.save(fn);
       repFun.flush(); // Whoa! "deleted instance passed to merge"
   }

  void doSomeDelegation(Test test) {
    repTest.delete(test.getId()); //in this example this will delete Test #76
  }

但是,为此,我将不得不加载一个 Department 对象,很可能与它的员工集合一起加载(这将导致另一个查询,除非它是预先加载的),否则我可能不需要该数据本次交易。更重要的是,可能还有其他实体引用该员工,所以我必须知道所有可能的引用(这可能是合理的,但有点味道),并通过它们,检查引用端的所有对象(假设可能有一个或多个项目引用该员工,那么我们将需要进行查询以找到所有实际引用它的项目,然后遍历所有发现的实体,将它们与它们的引用一起加载,并更新这些引用) .

这是相当多的工作要做,最令人沮丧的部分是实际上没有必要保持有效的数据库状态!好吧,如果从 Department 到 Employee 的引用是映射到数据库列(即 Department.employee)的属性,而不是集合,那么取消这个引用总是必不可少的;但是如果该引用是集合的一部分,并且集合本质上映射到 Employee 行,那么从 DB 中删除 Employee 实际上就足以拥有一个有效的数据库。无需更新部门,或查找和更新多个项目。更重要的是:如果这样的部门不在 EntityManager 中,那么我们仍然可以跳过更新它,而不是搞砸 JPA 持久性! (但不确定版本更新)。

一个可能的解决方案听起来像“知道可能有部门实体引用被删除的员工,只找到那些已经在 EntityManager 中的实体,然后从他们的引用集合中删除员工”,但我不认为我知道该怎么做,此外,执行删除这样简单的事情是一个非常复杂的逻辑。

那么,在上面的示例中,正确的做法是什么?观察到本应有经验的开发人员如何无法对这个非常简单的问题提出一个好的答案,这真的很有趣。

【问题讨论】:

  • 您确实负责部门内员工集合的一致性。但是,如果您的用例是“删除员工”,则此用例中根本不涉及部门,您无需关心部门。只需删除员工,您就完成了。
  • 错了,看例子。当然,如果我们确定该事务中除了“按 id 删除”之外没有其他逻辑,那么您是对的。但是,除非您的应用程序特别琐碎,否则您无法始终确定这一点。在前面的示例中,doSomeDelegation 甚至可能是某个其他类的一部分。它甚至可能没有接收到作为参数的删除目标,但由于一些与 test() 无关的孤立逻辑,意外地决定删除这个对象。由于 test() 没有显式添加相同的测试,所以应该没有冲突,但它确实发生了。
  • 当然,如果我们确定此事务中除了“按 id 删除”之外没有其他逻辑:那么,您应该知道这一点。如果您知道部门及其员工列表涉及删除员工的用例,那么用例应该确保保持一致性。关键是:这是业务逻辑的责任。低级 deleteById() 存储库方法不负责维护所有可能的关联。
  • 好的我明白你的回答,谢谢。所以我不能将我的一些服务用作黑匣子,而必须确切地知道它们在做什么。这肯定可以解决问题,但我不能完全同意你的看法。希望看到其他建议。
  • 但是我想即使我不必记住每个 API 调用中发生的所有事情,但我真的必须在我要在其中进行更改时刷新这些知识方法,因为除了偶然发现 JPA 限制之外,还有其他方法可以引入冲突。所以也许,也许你是对的。

标签: hibernate jpa spring-data spring-data-jpa


【解决方案1】:

在 JPA 中有引用之类的东西。 EntityManager 有方法getReference。此参考使您能够对实体执行任何操作,而无需获取除 id 之外的任何数据。如果你需要删除引用的数据——它可以在两种级联情况下工作——hibernate 和 db 的情况。

【讨论】:

  • 引用不能解决问题。我的意思是,看,我们得到了实体 A。假设 A.b = 某个 B。我们可以在不获取 B 的情况下从 A 转到 B,是的,但是你不能在没有最终加载 B 的情况下从 B.a 中删除 A。因为,就我而言知道,访问 getId() 以外的任何代理方法都会触发加载实际实体。
  • @MaksimGumerov 但我们可以设置删除孤儿,如下所示:objectdb.com/java/jpa/persistence/delete#Orphan_Removal_
  • 嗯。孤儿是关系的另一方不再引用的实体。如果我们从 Department.employees 中提取 Employee(而不是从 EM 中删除),并且他们的关系被标记为 orphanRemoval,那么 Employee 就会变成孤儿并被删除,对吗?这如何帮助我做到这一点 - 从 Department.employees 中删除 Employee?
  • @MaksimGumerov 好吧,现在我想我不能正确理解你的任务。你的任务到底是什么?删除一名具体员工?或者如果没有更多员工,则删除部门。还是在不获取相关信息的情况下从部门中删除员工?
  • 假设您要编写一些辅助函数来执行包括删除一名员工在内的操作。如果有机会在此事务中与同一个部门进行另一个 persist() 调用,你如何在不加载其部门的情况下做到这一点。
【解决方案2】:

部门是参考数据。您不应通过Department 控制任何相关实体。所以你不应该在Department 中有employees 关联。这与在Role 实体中拥有users 关联是一回事。

如果您仍想拥有employees 关联

  1. 不要在Department 部分对employees 使用任何级联。

  2. 不要将 每个请求的会话反模式 甚至 持久上下文模式 视为不可动摇的东西。您可以考虑使用这种方法:仅为简单的 CRUD 操作(如 get 或 merge)打开持久上下文,并使用持久上下文之外的全部对象。

【讨论】:

  • 嗯,使用不同的持久上下文是一种选择,但是这样对一个上下文的更改不会传播到另一个,对吧?因此,如果我的示例中的“doSomeDelegation”在单独的上下文中工作,它不会看到在“test”的外部上下文中累积的更改(它们甚至可能还没有被刷新!),并且不止:在某些情况下(取决于隔离级别)“测试”上下文也不会看到“doSomeDelegation”的变化。这很糟糕,尽管有时可以接受。
  • 不使用级联 - 会起作用,但如果模型在其他方面是正确的,那么仅仅为了启用一项特定操作而全局更改模型是不正确的。现在到“部门不应该有员工协会”的问题:为什么不呢?你说部门是参考数据,但它们不一定是。它们很可能是主数据,它们可能有累积的统计数据、它们之间的关系、对它们的操作——对员工来说也是如此。那么为什么他们不是一等人,为什么他们不应该与员工有反向关系呢?
  • @MaksimGumerov 它不会看到外部上下文中累积的变化——你不应该在上下文中做任何这样的变化!仅允许 CRUD 操作。
  • @MaksimGumerov 不使用级联 - 可以,但全局更改模型是不对的 - cascade 是 Hibernate 特定的东西,这不是模型。
  • @MaksimGumerov 它们很可能是主数据,它们可能有累积的统计数据、它们之间的关系、对它们的操作——其他模型类也可以用于此。这些类可以与Department 有关系。
【解决方案3】:

1) 似乎没有办法从 EntityManager 中已经存在的所有引用集合中自动提取实体,而无需不必要地加载更多实体。当然,除了 EntityManager.clear()

2) 这会使重用 doSomeDelegation() 变得更加困难,因为它无法预测哪些代码将在同一个事务中运行。但是,调用代码可以预测 doSomeDelegation() 可能会受到干扰 - 只要它知道 doSomeDelegation(test) 删除实体“test”(它应该是方法合同的一部分,因为“test”可以通过其他人 - 它不是 doSomeDelegation 私有的东西)并且它通过 JPA 工作(这种知识来自对环境和配置的了解)。

3) 因此,编写上面显示的代码的正确方法是知道 doSomeDelegation 通过 JPA 删除“test”,并且由于 JPA 的合同,这必须在调用代码中考虑 - 即 fn.getTests ().remove()

这(第 3 部分)是显而易见的解决方案,但我建议调用代码完全了解被调用者中发生的事情是不正确的,但第 2 部分解释了为什么不需要完全了解被调用者相关的影响。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-14
    相关资源
    最近更新 更多