10

When providing #equals implementation for a UDT in Java one of the conditions is that the passed argument object must be an instance of the current class otherwise we fail-fast return false see Effective Java (EJ2). However, while using Hibernate 4 we can end up with javassist proxy instances due to lazy loading where this #equals condition will fail. What would be the best choice to overcome this? The few choices I can think of are:

  • extend the equals implementation to take into account the proxy case. Cons: maintainability toll, hardwired dependency to Hibernate proxy infrastructure, hacky, entity or domain models should be agnostic to the ORM being used i.e. since they might be reused in different contexts where there is no need for ORM e.g. Swing UI.
  • check whether it is a proxy before invoking equals. Cons: not always possible, i.e., dealing with Collections and implicit invocations of equals, e.g., Map.
  • Refrain from using lazy loading. Cons: not reasonable nor efficient in all use-cases.

UPDATE

Reviewing EJ2 again I believe that the following will work for all scenarios (Type-Type, Type-Proxy, Proxy-Type and Proxy-Proxy) but as pointed out in one of the comments below it may loop forever if the Type is compared to a totally different type e.g. Person.equals(Employee) and both use the same equals EJ2 criteria.

    if (this.getClass() != anObject.getClass())
    {
        return anObject.equals(this);
    }
Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
SkyWalker
  • 13,729
  • 18
  • 91
  • 187
  • just making sure, the proxy will extend your entity's class ? – Aviram Segal Dec 24 '12 at 18:12
  • proxy instances don't extend, see `InvocationHandler` and the condition `(this.getClass() == anObject.getClass())` will, of course, evaluate to false. – SkyWalker Dec 24 '12 at 18:16
  • 1
    You left off an option: ignore the Effective Java advice and allow subclasses in .equals(). – Ryan Stewart Dec 24 '12 at 18:20
  • I ment the proxy can be assigned to the entity type, for example entity `A` then `A a = proxy;` – Aviram Segal Dec 24 '12 at 18:25
  • 1
    @GiovanniAzua Are you sure you're not confusing proxy technologies? `InvocationHandler`s are something that the JDK uses to proxy interfaces. When Hibernate proxies your classes, I'm 99% certain it generates a subclass of your entity class. (It does when using CGLIB, and I can't imagine any way how this could possibly work otherwise with Javassist, considering that the code Aviram mentions has to work.) – millimoose Dec 24 '12 at 18:25
  • The code at the and, if you put it in 2 different classes and invoke equals wouldn't it run forever ? – Aviram Segal Dec 24 '12 at 18:32
  • true :( only works for the same type or its proxy. – SkyWalker Dec 24 '12 at 18:34
  • 1
    My advice: relax the EJ2 condition to `if (other instanceof ThisClass)`. Actually, if you make the `equals()` method `final`, I believe the method will be formally correct. (This will prevent subclasses from changing the equality semantics of course - the formal correctness hinges on this.) – millimoose Dec 24 '12 at 19:12
  • See also Angelika Langer's treatment of this: http://www.angelikalanger.com/Articles/JavaSolutions/SecretsOfEquals/Equals.html – millimoose Dec 24 '12 at 19:12

4 Answers4

11

I stumbled on the same problem. The way I fixed is was to change the .equals-method.

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (!getClass().isAssignableFrom(obj.getClass()))
        return false;
    AbstractEntity other = (AbstractEntity) obj;
    if (getId() == null) {
        if (other.getId() != null)
            return false;
    } else if (!getId().equals(other.getId()))
        return false;
    return true;

The trick is to not compare the classes to be the same but use the isAssignableFrom-method. The other trick is to not use direct properties (other.id), but use the get-method (other.getId())

Willem de Wit
  • 8,604
  • 9
  • 57
  • 90
  • 1
    getClass().isAssignableFrom(obj.getClass()) always returns false to me – usr-local-ΕΨΗΕΛΩΝ Oct 29 '13 at 14:30
  • This did the trick for me. For those interested: here is how to test it: Employee employeeRef = em.getReference(Employee.class, id); Employee employee = em.find(Employee.class, id); Assert.assertTrue(employee.equals(employeeRef)); – Alan B. Dee Sep 05 '14 at 17:35
  • makes absolutelly no sense, hibernate proxy wont be assignable to the original class... this trick WONT WORK... this might work for general inheritance, not to hibernate proxy comparsion – Rafael Lima Jun 30 '22 at 23:40
9

I don't have reputation to comment the Willem de Wit answer. Than I need to post a new answer.

To solve the issue of djechelon, you should replace this line:

if (!getClass().isAssignableFrom(obj.getClass()))

for

if ( !obj.getClass().isAssignableFrom(getClass()) && !getClass().isAssignableFrom(obj.getClass()) )

Then you will assure that the equals will work for all scenarios (Type-Type, Type-Proxy, Proxy-Type and Proxy-Proxy).

I don't have reputation to vote up your answer too. I'm so miserable!

DHansen
  • 225
  • 3
  • 13
1

You can do two things: 1. Change the equals to use instanceof instead of class equality. The type of the proxy is not equal to the type of the entity, but rather extends the type of the entity.

  1. Unwrap the proxy to get the entity itself (there are several hibernate tools that helps you do that)
Doron Manor
  • 576
  • 4
  • 5
1

The answer by DHansen above is close, but for me (using Hibernate) this solved the issue:

if (!Hibernate.getClass(this).equals(Hibernate.getClass(obj))) { return false; } as suggested by Dr. Hans-Peter Störr

Also it is important to always use 'getters' as suggested by Willem de Wit above.

Community
  • 1
  • 1
Lars
  • 1,311
  • 1
  • 11
  • 11