简体   繁体   English

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

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

I have two JPA entity classes, Group and User 我有两个JPA实体类,Group和User

Group.java: 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: 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;
    }
}

I tried to run the following snippet for a group with one member, where group is an entity just retrieved from JpaRepository and user is the member of that group and detached entity. 我尝试为具有一个成员的组运行以下代码段,其中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

After some debugging I found out that User.equals() was called during .contains() call, but the user in Hibernate collection had null fields, and thus .equals() evaluated to false. 一些调试后我发现User.equals()时被称为.contains()调用,但在Hibernate中收集用户不得不空字段,从而.equals()结果为假。

So why it is so odd and what is the correct way of calling .contains() here? 那么为什么它如此奇怪以及在这里调用.contains()的正确方法是什么?

There's a couple of pieces to this puzzle. 这个难题有几个部分。 First, the fetch type for @ManyToMany associations is LAZY . 首先, @ManyToMany关联的获取类型是LAZY So in your Group, the members field employs lazy loading. 因此,在您的组中, members字段使用延迟加载。 When lazy loading is used, Hibernate will use proxies for objects to only do the actual loading on accessing them. 当使用延迟加载时,Hibernate将使用对象的代理仅在访问它们时执行实际加载。 The actual collection is most likely some implementation of PersistentBag or PersistentCollection (forgot which, and the Hibernate javadocs seem inaccessible at the moment) which does some of the magic behind your back. 实际的集合很可能是PersistentBagPersistentCollection一些实现(忘了哪些,以及Hibernate javadocs目前似乎无法访问),这些实现了你背后的一些魔力。

Now, you may wonder, when you're calling group.getMembers() shouldn't you then get the actual collection and be capable of using it without worrying about its implementation? 现在,您可能想知道,当您调用group.getMembers()时,您是否应该获得实际的集合并且能够使用它而不必担心它的实现? Yes, but there's still a catch to the lazy loading. 是的,但仍然有一个延迟装载的问题。 You see, the objects in the collection themselves are proxies, which initially only have their identifier loaded but not other properties. 你看,集合中的对象本身就是代理,它们最初只加载了它们的标识符,但没有加载其他属性。 It's only on accessing such a property that the full object is initialized. 只有在访问这样的属性时才会初始化完整对象。 This allows Hibernate to do some clever things: 这允许Hibernate做一些聪明的事情:

  • It lets you check the size of the collection without having to load everything. 它允许您检查集合的大小,而无需加载所有内容。
  • You can fetch only the identifiers (primary keys) of objects in the collection without the whole object being queried. 您只能获取集合中对象的标识符(主键),而不会查询整个对象。 The foreign keys are normally quite efficient to get when the parent object is being loaded with a join, and are used for a lot of things, such as checking whether an object is known in the persistence context. 当父对象使用连接加载时,外键通常非常有效,并且用于很多事情,例如检查对象是否在持久化上下文中是已知的。
  • You can get a specific object in the collection and have that initialized without every object in the collection needing to be initialized. 您可以在集合中获取特定对象并初始化该集合,而不需要初始化集合中的每个对象。 Although this can result in many queries (the "N+1 problem") it can also make sure that not more data is sent over the network and loaded in memory than needed. 虽然这可能导致许多查询(“N + 1问题”),但它也可以确保通过网络发送的数据不会超过所需的数据并加载到内存中。

The next piece of the puzzle is that in your User class, you've used property access instead of field access. 下一个难题是,在您的User类中,您使用了属性访问而不是字段访问。 Your annotations are on the getters instead of the fields (like in Group ). 您的注释位于getter而不是字段(如Group )。 Maybe this has changed, but in at least some older versions of Hibernate, getting only the identifier via the proxies only worked with property access, because the proxy operates by substituting the methods, but can't circumvent field access. 也许这已经改变,但至少在一些旧版本的Hibernate中,只通过代理获取标识符只能使用属性访问,因为代理通过替换方法来操作,但不能绕过字段访问。

So what happens is that in your equals method this part probably works fine: if (id != user.id) return false; 所以会发生在你的equals方法中这部分可能正常工作: if (id != user.id) return false;

... but this doesn't: return email.equals(user.email); ...但这不是:return email.equals(user.email);

You might as well have gotten a nullpointer exception there it didn't so happen that the contains method calls the equal on the provided object (your filled-in, detached user) with its collection entries as argument. 你可能也得到了一个nullpointer异常,并没有发生这样的情况,即contains方法在提供的对象(你的填充,分离用户)上调用相等的,其集合条目作为参数。 The other way around could've cause a nullpointer. 反过来可能导致nullpointer。 This is the final piece of the puzzle. 这是拼图的最后一部分。 You're using the fields directly here instead of using the getter for email, so you're not forcing Hibernate to load the data. 你在这里直接使用这些字段而不是使用getter来获取电子邮件,所以你不是强迫Hibernate加载数据。

So here's some experiments you may perform. 所以这是你可能会进行的一些实验。 I'd try them myself, but it's getting late here and I must be going. 我自己尝试一下,但现在已经很晚了,我一定要去。 Let me know what the outcome is, to see if my answer is correct and make it more useful for later visitors. 让我知道结果是什么,看看我的答案是否正确,并使其对后来的访问者更有用。

  • Change the property access in User to field access by putting the JPA/Hibernate annotations on the fields. 通过在字段上放置JPA / Hibernate注释,将User的属性访问权限更改为字段访问权限。 Unless this has changed in recent versions, it should cause all properties of the User instances to be initialized when accessing the collection, rather than just proxies with the identifiers filled in. This might not work anymore, however. 除非在最近的版本中已经更改,否则它应该在访问集合时初始化User实例的所有属性,而不仅仅是填充了标识符的代理。但是,这可能不再起作用。
  • Try getting that user1 instance from the collection first via the iterator. 首先尝试通过迭代器从集合中获取该user1实例。 Seeing how you didn't do explicit property access, I strongly suspect that getting an iterator on the collection and fetching an element from it also forces initialization of that element. 看看你是如何不进行显式属性访问的,我强烈怀疑在集合上获取迭代器并从中获取元素也会强制初始化该元素。 The Java implementation of contains for a List, for example, calls indexOf which just goes through the internal array, but doesn't call any methods like get which could trigger initialization. 例如, contains List的Java实现调用indexOf ,它只是通过内部数组,但不调用任何可能触发初始化的get方法。
  • Try using the getters in your equals method instead of direct field access. 尝试在equals方法中使用getter而不是直接字段访问。 I've found that when dealing with JPA, it's better to always use the getters and setters, even for methods in the class itself, to avoid issues like these. 我发现在处理JPA时,最好始终使用getter和setter,即使是类本身的方法,也要避免这样的问题。 As an actual solution, this is probably the most robust way. 作为一种实际的解决方案,这可能是最强大的方式。 Do make sure to handle cases where email may be null, though. 但请确保处理email可能为空的情况。

JPA does some crazy magic behind your back and tries to make it mostly invisible for you, but sometimes it comes back to bite you. JPA在你的背后做了一些疯狂的魔法,并试图使它对你来说几乎看不见,但有时它又回来咬你。 I'd dig a bit more in the Hibernate source code and run some experiments if I had the time, but I might revisit this later to verify the above claims. 如果我有时间的话,我会在Hibernate源代码中挖掘更多内容并运行一些实验,但我可能会在稍后再次访问以验证上述声明。

Many developer that use JPA, have problems with detached entities. 许多使用JP​​A的开发人员都遇到了分离实体的问题。 With Hibernate the most common problem is that you can't loop lazy loaded collections in your view, because the Persistence context was closed after your controller/service was called, which is before the view starts to render. 使用Hibernate时,最常见的问题是您无法在视图中循环延迟加载的集合,因为在调用控制器/服务之后,在视图开始渲染之前,Persistence上下文已关闭。 As you can imagine this is something almost every Hibernate JPA developer will run into, and it is such a retarded behavior that EclipseLink (the JPA reference implementation) has actually decided to violate the JPA spec in this area, as the IndirectList can actually load data after the Persistence Context is closed - I like EclipseLink! 正如您可以想象的那样,几乎每个Hibernate JPA开发人员都会遇到这种情况,并且EclipseLink(JPA参考实现)实际上已经决定违反此区域中的JPA规范,因为IndirectList实际上可以加载数据持久化上下文关闭后 - 我喜欢EclipseLink!

The above answer as to why your unmanaged user is not equal to your managed user is correct, but I would be more concerned with WHY you are comparing a managed and an unmanaged version of the same logical entity! 上面的答案为什么你的非托管用户不等于你的托管用户是正确的,但我会更关心你为什么要比较同一个逻辑实体的托管和非托管版本! Within a persistence context all references to an entity must be the same, if you had merged the detached user, the instance returned by merge was guaranteed to be the same as the already managed user with the same id. 在持久化上下文中,对实体的所有引用必须相同,如果已合并分离的用户,则merge返回的实例保证与具有相同ID的已管理用户相同。 If two user instances are both managed, you can use == to check if they are the same logical entity. 如果两个user实例都是托管的,则可以使用==检查它们是否是同一个逻辑实体。

Another common detachment problem occurs because developers use their Entities as DTOs and create instances directly from JSon using @RequestBody . 另一个常见的脱离问题是因为开发人员使用他们的实体作为DTO并使用@RequestBody直接从JSon创建实例。 If you know your way around JPA, detached entities and merging this is certainly doable, but I think that most JPA developers which are capable of doing this correctly, knows that creating separate DTOs saves you from a lot of strange errors - There are simply different requirements to Entities and DTOs, and I recommend that you don't mix the two. 如果您了解JPA的方式,那么分离的实体和合并它肯定是可行的,但我认为大多数能够正确执行此操作的JPA开发人员都知道创建单独的DTO可以避免许多奇怪的错误 - 只是有所不同对实体和DTO的要求,我建议你不要混淆这两者。

/JPA wizard since 2009 / JPA向导自2009年以来

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM