【问题标题】:JPA entity with collection returns false for contains method on detached member带有集合的 JPA 实体对于分离成员上的包含方法返回 false
【发布时间】:2017-09-18 18:38:52
【问题描述】:

我有两个 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

}

用户.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;
    }
}

我尝试为具有一个成员的组运行以下 sn-p,其中 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

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

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

【问题讨论】:

  • 您声明 Hibernate 集合中的用户有空字段。哪些字段为空?电子邮件字段?通过迭代器获取用户后它仍然为空吗?如果 Group 对其成员集合使用延迟加载(我相信这是 @ManyToMany 的默认行为),它可能会加载一些代理或仅填充主键的对象,只有在您对集合执行访问时才会获取其他字段。

标签: java hibernate jpa collections contains


【解决方案1】:

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

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

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

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

所以发生的情况是,在您的 equals 方法中,这部分可能工作正常:if (id != user.id) return false;

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

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

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

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

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

【讨论】:

  • 我不认为集合中的对象本身是代理的声明是正确的,因为这需要动态字节码生成。我相当确定PersistentBag 包含真实的实体实例(因为 JPA 要求在持久化上下文中只有一个实体表示,但确实可能不会加载属性和外部实体。
  • 如果加载需要在属性访问时触发,难道不需要代理吗?还是在从 PersistentBag 获取条目时加载属性?
  • 我尝试将注释移动到字段中,但没有任何改变。然后我用User.equals() 中的getter 替换了字段访问并且它起作用了,尽管我很失望我的POJO 一定不是POJO。 “这部分可能工作正常:if (id != user.id)” - id 也为空(零)。
  • @A.K.感谢您的反馈。实际上,JPA 的工作方式在很大程度上允许您将实体用作简单的 Java 对象,但在某些情况下,它的背景魔法会渗透并导致奇怪的行为。尤其是缓存和反射可能会导致一些陷阱。
【解决方案2】:

许多使用 JPA 的开发人员都遇到了分离实体的问题。使用 Hibernate,最常见的问题是您无法在视图中循环延迟加载的集合,因为在调用控制器/服务之后,即在视图开始呈现之前,Persistence 上下文已关闭。正如您所想象的,这是几乎每个 Hibernate JPA 开发人员都会遇到的问题,EclipseLink(JPA 参考实现)实际上决定在这方面违反 JPA 规范,因为 IndirectList 实际上可以在 Persistence Context 关闭后加载数据 - 我喜欢 EclipseLink!

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

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

/自 2009 年以来的 JPA 向导

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-09
    • 2015-12-30
    • 1970-01-01
    • 2015-09-06
    • 1970-01-01
    • 1970-01-01
    • 2013-12-14
    • 2015-03-18
    相关资源
    最近更新 更多