带有集合的JPA实体对分离成员上的contains方法返回false

A. *_* K. 3 java collections hibernate jpa contains

我有两个JPA实体类,Group和User

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

}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我尝试为具有一个成员的组运行以下代码段,其中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
Run Code Online (Sandbox Code Playgroud)

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

那么为什么它如此奇怪以及.contains()在这里调用的正确方法是什么?

G_H*_*G_H 5

这个难题有几个部分.首先,@ManyToMany关联的获取类型是LAZY.因此,在您的组中,该members字段使用延迟加载.当使用延迟加载时,Hibernate将使用对象的代理仅在访问它们时执行实际加载.实际的集合很可能是某些实现PersistentBagPersistentCollection(忘了哪些,以及Hibernate javadocs目前似乎无法访问),这些实现了背后的一些魔力.

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

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

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

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

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

你可能也得到了一个nullpointer异常,并没有发生这样的情况,即contains方法在提供的对象(你的填充,分离用户)上调用相等的,其集合条目作为参数.反过来可能导致nullpointer.这是拼图的最后一部分.你在这里直接使用这些字段而不是使用getter来获取电子邮件,所以你不是强迫Hibernate加载数据.

所以这是你可能会进行的一些实验.我自己尝试一下,但现在已经很晚了,我一定要去.让我知道结果是什么,看看我的答案是否正确,并使其对后来的访问者更有用.

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

JPA在你的背后做了一些疯狂的魔法,并试图使它对你来说几乎看不见,但有时它又回来咬你.如果我有时间的话,我会在Hibernate源代码中挖掘更多内容并运行一些实验,但我可能会在稍后再次访问以验证上述声明.