Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Equalsverifier always throw AssertionError on compare entities with Collection field #816

Closed
LepestokSakuri opened this issue May 19, 2023 · 7 comments

Comments

@LepestokSakuri
Copy link

Describe the bug
Going Equalsverifier from 3.9 to 3.14.1 and now my tests failed with this error:
java.lang.AssertionError: EqualsVerifier found a problem in class com.myproject.security.service.domain.Client.
-> JPA Entity: direct reference to field clientTypes used in equals instead of getter getClientTypes.

To Reproduce
Client.java

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.hibernate.annotations.Where;

import javax.persistence.*;
import java.util.Set;

@Data
@EqualsAndHashCode(callSuper = false, exclude = "clientTypes")
@ToString(exclude = "clientTypes")
@Entity
@Table(name = "clients")
public class Client extends BaseEntity {

    private String name;

    private String orgName;

    private String email;

    @OneToMany(mappedBy = "client")
    @Where(clause = "deleted = false")
    private Set<ClientType> clientTypes;
}

ClientType.java

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

import javax.persistence.*;

@Data
@EqualsAndHashCode(callSuper = false, exclude = {"client"})
@ToString(exclude = {"client"})
@Entity
@Table(name = "client_types")
public class ClientType extends BaseEntity {

    private boolean attorney;

    private String attorneyName;

    @ManyToOne
    @JoinColumn(name = "client_id")
    private Client client;

}

Code that triggers the behavior

public class BasicModelTests {

    private final static String DOMAIN_PACKAGE = User.class.getPackage().getName();

    @Test
    public void domainPackageModelEqualsAndHashCodeTest() throws Exception {
        ImmutableSet<ClassPath.ClassInfo> classInfos = ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive(DOMAIN_PACKAGE);

        Client client1 = new Client();
        client1.setName("client1");
        Client client2 = new Client();
        client2.setName("client2");
        ClientType clientType1 = new ClientType();
        clientType1.setAttorneyName("clientType1");
        ClientType clientType2 = new ClientType();
        clientType2.setAttorneyName("clientType2");

        for (ClassPath.ClassInfo classInfo : classInfos) {
            try {
                Class<?> c = classInfo.load();
                if (c.isEnum() || Modifier.isAbstract(c.getModifiers()) || classInfo.getSimpleName().endsWith("_")) {
                    continue;
                }
                EqualsVerifier.forClass(c).suppress(Warning.ALL_FIELDS_SHOULD_BE_USED).withRedefinedSuperclass()
                        .suppress(Warning.STRICT_INHERITANCE)
                        .suppress(Warning.NONFINAL_FIELDS)
                        .withPrefabValues(Client.class, client1, client2)
                        .suppress(Warning.BIGDECIMAL_EQUALITY)
                        .verify();
                assertNotNull(BeanUtils.instantiateClass(c).toString());
            } catch (AssertionError error) {
                log.error("Failed while processing class {}", classInfo.getName());
                throw error;
            }
        }
    }
}

Error message
java.lang.AssertionError: EqualsVerifier found a problem in class com.myproject.security.service.domain.Client.
-> JPA Entity: direct reference to field clientTypes used in equals instead of getter getClientTypes.

For more information, go to: https://www.jqno.nl/equalsverifier/errormessages
(EqualsVerifier 3.14.1, JDK 17.0.2 on Windows 10)

at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:314)
at com.myproject.security.service.BasicModelTests.domainPackageModelEqualsAndHashCodeTest(BasicModelTests.java:84)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)

Caused by: nl.jqno.equalsverifier.internal.exceptions.AssertionException
at nl.jqno.equalsverifier.internal.util.Assert.assertTrue(Assert.java:53)
at nl.jqno.equalsverifier.internal.checkers.fieldchecks.JpaLazyGetterFieldCheck.assertEntity(JpaLazyGetterFieldCheck.java:99)
at nl.jqno.equalsverifier.internal.checkers.fieldchecks.JpaLazyGetterFieldCheck.execute(JpaLazyGetterFieldCheck.java:62)
at nl.jqno.equalsverifier.internal.checkers.FieldInspector.check(FieldInspector.java:29)
at nl.jqno.equalsverifier.internal.checkers.FieldsChecker.check(FieldsChecker.java:99)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verifyWithExamples(SingleTypeEqualsVerifierApi.java:430)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.performVerification(SingleTypeEqualsVerifierApi.java:385)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:312)
... 70 more

Expected behavior
No AssertionError or another errors

Version
3.14.1

Additional context
Analyzing source code, I found that problem in JpaLazyGetterFieldCheck.java (confirmed with debug). Two objects for comparing create with the same way (using method getRedObject), after these objects compare with equals (I think that result will always true), so after that comparing I always getting error
Also I analyzed older versions of this file and in first version was comparing two different objects, because they was created with different methods (getRedObject and getBlueObject). With debug I created objects with this way and they was absolutely different.
Besides I tried to remove exclude in EqualsAndHashCode annotation. After this my tests finished correctly

@jqno
Copy link
Owner

jqno commented May 22, 2023

Version 3.12 (see Changelog) introduced a check for lazy fields. In your case, clientTypes has a @OneToMany annotation with fetchType = FetchType.LAZY (LAZY is the default). For lazy fields, it's important that equals calls the getter instead of the raw field, otherwise the lazy field might not be initialized, and you might run into issues where two objects are supposed to be equal, but one doesn't have its lazy field initialized yet and equals returns false.

In your case, I see you're using Lombok, and you actively exclude the lazy field. I don't understand why it still tries to reference clientTypes, but somehow it does. Without access to your code, I can't help you with that. Maybe it helps if you delombok the class to see what code is actually generated?

@LepestokSakuri
Copy link
Author

LepestokSakuri commented May 22, 2023

Thank you for your response
Generated classes in target folder have getters for fields with collection types
Later I try to add FetchType.LAZY but I think that it doesn't help me, because its default fetch type as you say

Also I want to know why you compare two same objects in JpaLazyGetterFieldCheck.java? In older versions of this file you compare two different objects which acquired with different methods

@jqno
Copy link
Owner

jqno commented May 22, 2023

It's good that your classes have getters, but that's not what matters. What matters, is that equals has to call these getters, instead of the fields, because calling the getter will initialize the lazy collection. Otherwise, equals is performed on uninitialized data, and this may result in incorrect behaviour, as I said in my previous post. EqualsVerifier checks this since version 3.12, so that explains why you're seeing this issue now, and not before you upgraded.

JpaLazyGetterFieldCheck compares identical objects because its purpose is to see if calling equals causes an exception to be thrown. If the objects are unequal, you risk that not all lazy fields are inspected for this exception, because equals might return early if one of the fields is false. JpaLazyGetterFieldCheck does exactly what it must do.

In your case, I see that EqualsVerifier says that you have a lazy field (clientTypes) and that your equals method is calling it directly, instead of through the getter. I also see that you have instructed Lombok to exclude this field from the equals method. This is strange, because why does your equals method call this field if it's excluded? I don't know. That why I asked you to use delombok, so you can see the actual equals method that's being generated by Lombok. Maybe it's called indirectly, through one of its fields?

If inspecting the output of delombok doesn't help, I recommend that you put the delomboked Java code somewhere and run EqualsVerifier on that, and see with the debugger where the reference to clientType occurs.

@jqno
Copy link
Owner

jqno commented Jul 8, 2023

I've just released EqualsVerifier 3.15, where using getters in equals is always required for @ManyToOne fields. Although I don't think that will affect your specific case. Is this still an open issue for you?

@MikeTraceur
Copy link

MikeTraceur commented Jul 24, 2023

Tested with equalsverifer version 3.15:
User.java

@Data
@Table(name = "user")
@Entity
@EqualsAndHashCode(exclude = {
        "oneToManyRoleList", "manyToManyRoleList", "manyToOneRole", "oneToOneRole"})
public class User {

    @Id
    private Long id;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "role_id")
    private List<Role> oneToManyRoleList = new ArrayList<>();

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_role_table",
            joinColumns = {@JoinColumn(name = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "role_id")}
    )
    private List<Role> manyToManyRoleList = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "role_id", nullable = false)
    private Role manyToOneRole;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "role_id", nullable = false)
    private Role oneToOneRole;

}

Role.java

@Data
@Entity
@EqualsAndHashCode
public class Role {

    @Id
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<User> userList;
}

UserTest.java

class UserTest {

    @Test
    void userTest() {
        Role red = new Role();
        Role blue = new Role();
        red.setId(1L);
        blue.setId(2L);
        EqualsVerifier.forClass(User.class)
            .withPrefabValues(Role.class, red, blue)
                .suppress(Warning.ALL_FIELDS_SHOULD_BE_USED)
                .verify();
    }

}

Error:

java.lang.AssertionError: EqualsVerifier found a problem in class de.bund.bamf.helper.equalizer.User.
-> JPA Entity: direct reference to field oneToManyRoleList used in equals instead of getter getOneToManyRoleList.

For more information, go to: https://www.jqno.nl/equalsverifier/errormessages
(EqualsVerifier 3.15, JDK 11.0.10 on Windows 10)

User.java (Delombok):

    public boolean equals(final Object o) {
        if (o == this) return true;
        if (!(o instanceof User)) return false;
        final User other = (User) o;
        if (!other.canEqual((Object) this)) return false;
        final Object this$id = this.getId();
        final Object other$id = other.getId();
        if (this$id == null ? other$id != null : !this$id.equals(other$id)) return false;
        return true;
    }

    protected boolean canEqual(final Object other) {
        return other instanceof User;
    }

    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final Object $id = this.getId();
        result = result * PRIME + ($id == null ? 43 : $id.hashCode());
        return result;
    }

None of the excluded fields are inside the equals method.
If the fields aren't excluded, the test goes green.
But it makes sense to exclude Collections for equals/hashcode for performance in jpa entities.

@jqno
Copy link
Owner

jqno commented Jul 25, 2023

I'm on vacation right now. I'll look into this when I get back!

@jqno
Copy link
Owner

jqno commented Aug 2, 2023

Thanks, I was able to reproduce the issue now. I've just released version 3.15.1 which fixes this!

@jqno jqno closed this as completed Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants