集合 returns false 的 JPA 实体包含分离成员上的方法

JPA entity with collection returns false for contains method on detached member

我有两个 JPA 实体 类、组和用户

Group.java:

@Entity
@Table(name = "groups")
public class Group {

    @Id
    @GeneratedValue
    private int id;


    @ManyToMany
    @JoinTable(name = "groups_members", joinColumns = {
            @JoinColumn(name = "group_id", referencedColumnName = "id")
    }, inverseJoinColumns = {
            @JoinColumn(name = "user_id", referencedColumnName = "id")
    })
    private Collection<User> members;


    //getters/setters here

}

User.java:

@Entity
@Table(name = "users")
public class User {

    private int id;
    private String email;

    private Collection<Group> groups;

    public User() {}

    @Id
    @GeneratedValue
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Column(name = "email", unique = true, nullable = false)
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "groups_members", joinColumns = {
            @JoinColumn(name = "user_id")
    }, inverseJoinColumns = {@JoinColumn(name = "group_id")})
    public Collection<Group> getGroups() {
        return groups;
    }

    public void setGroups(Collection<Group> groups) {
        this.groups = groups;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;

        User user = (User) o;

        if (id != user.id) return false;
        return email.equals(user.email);
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + email.hashCode();
        return result;
    }
}

我尝试 运行 以下代码片段用于一个只有一个成员的组,其中 group 是刚从 JpaRepository 检索到的实体,user 是该组的成员并已分离实体。

            Collection<User> members = group.getMembers();
            System.out.println(members.contains(user)); //false
            User user1 = members.iterator().next();
            System.out.println(user1.equals(user)); //true

经过一些调试我发现 User.equals().contains() 调用期间被调用,但是 Hibernate 集合中的用户有空字段,因此 .equals() 评估为 false。

那么为什么这么奇怪,在这里调用 .contains() 的正确方法是什么?

这个谜题有几个部分。首先,@ManyToMany 个关联的提取类型是 LAZY。因此,在您的组中, members 字段采用延迟加载。当使用延迟加载时,Hibernate 将使用对象的代理来仅在访问它们时进行实际加载。实际的集合很可能是 PersistentBagPersistentCollection 的某种实现(忘了是哪个,Hibernate javadoc 目前似乎无法访问),它在你背后发挥了一些作用。

现在,您可能想知道,当您调用 group.getMembers() 时,您不应该获得实际的集合并能够使用它而不用担心它的实现吗?是的,但是延迟加载仍然有一个问题。你看,集合中的对象本身就是代理,它们最初只加载了它们的标识符,但没有加载其他属性。只有在访问这样一个 属性 时,整个对象才会被初始化。这允许 Hibernate 做一些聪明的事情:

  • 它让您无需加载所有内容即可检查集合的大小。
  • 您可以只获取集合中对象的标识符(主键),而无需查询整个对象。当通过连接加载父对象时,获取外键通常非常有效,并且用于很多事情,例如检查对象在持久性上下文中是否已知。
  • 您可以获得集合中的特定对象并对其进行初始化,而无需初始化集合中的每个对象。虽然这会导致许多查询("N+1 problem"),但它也可以确保通过网络发送和加载到内存中的数据不会超过需要。

下一个难题是,在您的 User class 中,您使用了 属性 访问权限而不是字段访问权限。您的注释位于 getter 上,而不是字段(如 Group 中)。也许这已经改变,但至少在一些旧版本的 Hibernate 中,仅通过代理获取标识符仅适用于 属性 访问,因为代理通过替换方法来运行,但不能绕过字段访问。

那么在你的 equals 方法中,这部分可能工作正常:if (id != user.id) return false;

...但这不是:return email.equals(user.email);

你可能还得到了一个空指针异常,contains 方法并没有调用提供的对象(你填写的、分离的用户)上的 equal 并将其集合条目作为参数。反过来可能会导致空指针。这是拼图的最后一块。您在此处直接使用字段而不是使用 getter 用于电子邮件,因此您不会强制 Hibernate 加载数据。

所以这里有一些你可以做的实验。我会自己尝试,但这里已经很晚了,我必须走了。让我知道结果是什么,看看我的回答是否正确,并使它对以后的访问者更有用。

  • 通过在字段上放置 JPA/Hibernate 注释,将 User 中的 属性 访问权限更改为字段访问权限。除非这在最近的版本中有所改变,否则它应该会导致在访问集合时初始化 User 实例的所有属性,而不仅仅是填充了标识符的代理。但是,这可能不再有效。
  • 首先尝试通过迭代器从集合中获取 user1 实例。看到您没有进行显式 属性 访问,我强烈怀疑在集合上获取迭代器并从中获取元素也会强制初始化该元素。例如,List 的 contains 的 Java 实现调用 indexOf 只是通过内部数组,但不调用任何可能触发的方法,例如 get初始化。
  • 尝试在 equals 方法中使用 getters 而不是直接字段访问。我发现在处理 JPA 时,最好始终使用 getters 和 setter,即使对于 class 本身中的方法也是如此,以避免此类问题。作为一个实际的解决方案,这可能是最可靠的方式。不过,请确保处理 email 可能为空的情况。

JPA 在你的背后施展了一些疯狂的魔法,并试图让它对你几乎不可见,但有时它会回来咬你一口。如果有时间,我会深入研究 Hibernate 源代码和 运行 一些实验,但稍后我可能会重新访问它以验证上述声明。

许多使用 JPA 的开发人员都遇到分离实体的问题。使用 Hibernate 最常见的问题是您不能在视图中循环延迟加载的集合,因为持久性上下文在调用 controller/service 之后关闭,这是在视图开始呈现之前。正如您可以想象的那样,几乎每个 Hibernate JPA 开发人员都会 运行 遇到这种情况,并且 EclipseLink(JPA 参考实现)实际上已经决定违反该领域的 JPA 规范,因为 运行 =10=] 在 Persistence Context 关闭后实际上可以加载数据 - 我喜欢 EclipseLink!

上面关于为什么你的非托管用户不等于你的托管用户的回答是正确的,但我更关心你为什么要比较同一逻辑的托管和非托管版本entity! 在持久性上下文中,对实体的所有引用必须相同,如果您合并了分离的用户,则 merge 返回的实例保证与已管理的用户相同同一个身份证。如果两个 user 实例都被托管,您可以使用 == 来检查它们是否是相同的逻辑实体。

另一个常见的分离问题是因为开发人员将他们的实体用作 DTO 并使用 @RequestBody 直接从 JSon 创建实例。如果您了解 JPA、分离实体和合并它当然是可行的,但我认为大多数能够正确执行此操作的 JPA 开发人员都知道创建单独的 DTO 可以避免许多奇怪的错误 - 它们只是不同对实体和 DTO 的要求,我建议您不要将两者混用。

/自 2009 年以来的 JPA 向导