Hibernate HHH000179 warning: Narrowing proxy to class this operation breaks ==

In this post I will explain why Hibernate is generating the HHH000179 warning and when ignoring it may introduce bugs in your code.

To understand what this “Narrowing proxy” is all about, first we must learn about Hibernate proxies. When we read a value of lazy loaded property or when we call EntityManager::getReference Hibernate returns a proxy object. This proxy is an instance of a class that was generated at runtime using library like Javassit.

For example for a simple entity:

@Entity
@Table(name = "person")
public class Person extends BaseEntity {
    @Column(name = "person_name", nullable = false)
    private String name;

    @ManyToOne(optional = false, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "house_id")
    private House house;

    @OneToMany(mappedBy = "owner", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Pet> pets = new HashSet<>(0);

    // ...
}

Generated proxy class looks similar to:

public class Person_$$_jvst5ed_2 
        extends Person 
        implements HibernateProxy, ProxyObject {
 
    private MethodHandler handler;
    private static Method[] _methods_;
 
    // plenty of other stuff here
 
    public final UUID _d7getId() {
        return super.getId();
    }
 
    public final UUID getId() {
        Method[] var1 = _methods_;
        return (UUID)this.handler.invoke(this, var1[14], var1[15], new Object[0]);
    }
}

TIP: In Hibernate 5.1 you may write generated proxy classes to disk by putting a breakpoint in JavassistProxyFactory::buildJavassistProxyFactory method and setting factory.writeDirectory field to a valid path. You may want to use a conditional breakpoint to avoid doing this manually every time a proxy is generated.

The most important point here is that proxy class extends entity class.

Now let’s see what happens when we mix proxies with inheritance. Given a simple class hierarchy:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "animal_type")
public abstract class Pet extends BaseEntity {
    @Column
    private String name;

    @JoinColumn(name = "owner_id", nullable = false)
    @OneToOne(optional = false, fetch = FetchType.LAZY)
    private Person owner;

    public abstract String makeNoise();
    // ...
}

@Entity
@DiscriminatorValue("cat")
public class Cat extends Pet { /* ... */ }

@Entity
@DiscriminatorValue("dog")
public class Dog extends Pet { /* ... */ }

When we use EntityManager::getReference to load a Pet we will get a proxy that extends Pet class because Hibernate does not know yet whatever our pet is a Cat or a Dog:

// In some earlier transaction:
Cat gerard = new Cat("gerard");
entityManager.persist(gerard);

gerardId = gerard.getId();

// In current transaction:
Pet pet = entityManager.getReference(Pet.class, gerardId);

assertThat(pet)
        .is(hibernateProxy())
        .is(uninitialized())
        .isInstanceOf(Pet.class)
        .isNotInstanceOf(Cat.class);

We may force Hiberante to query database to load proxied entity state but that doesn’t change proxy identity:

// makeNoise() will access field *via getter* to initialize proxy
logger.info("Pet is a cat: " + pet.makeNoise()); // meow meeeow

assertThat(pet)
        .isNot(uninitialized())
        .isNotInstanceOf(Cat.class);

Even though now Hibernate knows that our pet is a Cat it cannot change already loaded proxy class definition, Pet proxy continues to be so. This may cause you problems because tests like pet instanceof Cat will fail although pet indeed represents a cat.

There is also a second issue that may come up when working with proxies. If makeNoise() method would access pet data via field, proxy would not be notified about that data access and it wouldn’t load data from DB, causing our method to read an uninitialized field value. The moral is that we should always use getters and setters when dealing with entity state.

Now you may think that if we try to load Pet again (after proxy was initialized), Hibernate will return instance of the Cat entity. The behavior displayed by Hibernate is slightly different because of Hibernate first level cache that prefers returning already loaded entity instance than creating a new one:

Pet pet2 = entityManager.getReference(Pet.class, gerardId);

assertThat(pet2)
        .isNotInstanceOf(Cat.class)
        .isSameAs(pet);

What will happen when we try to explicitly load a Cat entity:

// HHH000179: Narrowing proxy to class Cat - this operation breaks ==
Pet pet3 = entityManager.getReference(Cat.class, gerardId);
assertThat(pet3)
        .isInstanceOf(Cat.class)
        .isNot(hibernateProxy());

Now we got the famous HHH000179 warning, and Hiberante handled us unproxied Cat instance. But why was this warning generated? Because right now we we have two different object (the proxy and the Cat instance) in our session that point to exactly the same entity.

Of course the pet proxy is pointing to the cat instance, and changes applied to e.g. entity instance are reflected in the proxy state:

assertThat(pet.getName())
    .isEqualTo("gerard");

assertThat(pet)
    .isNotSameAs(pet3);

// set via Cat entity
pet3.setName("proton");

// reflected via proxy
assertThat(pet.getName())
    .isEqualTo("proton");

So you may think that having two representation of the same DB row in memory is OK, but the real troubles begin if we do not override equals() and hashCode() methods properly. This is demonstrated by example:

// Alice is owner of the cat
Person alice = entityManager.find(Person.class, aliceId);

// Alice can own and not own the same cat...
assertThat(alice.getPets().contains(pet))
        .isFalse();

assertThat(alice.getPets().contains(pet3))
        .isTrue();

// But only if we rely on default equals() and 
// hashCode() implementation

Fortunately this can be easily fixed by providing equals() implementation that is based either on primary key or business key equality, for example:

@MappedSuperclass
public abstract class BaseEntity {
    @Id
    @Type(type="binary(16)")
    private UUID id;

    protected BaseEntity() {
        this.id = UUID.randomUUID();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || !(o instanceof BaseEntity)) return false;

        BaseEntity that = (BaseEntity) o;

        // remember to use *getters*
        return getId().equals(that.getId());
    }

    @Override
    public int hashCode() {
        return getId().hashCode();
    }
}

We may also reproduce above behaviour with lazy loading, you can find an example of how to do this in the attached source code.

Significance in the real world application

Recently I developed a module in an application that was based on huge in-house framework (Ughhh). This framework let’s call it X contained some of the entities that we used, but we have no way of modifying them. The only way to add some fields to an already existing entity was to extend it (fortunately for us, most entities in X were declared as base classes with inheritance strategy SINGLE_TABLE). At the end of this project we had plenty of small class hierarchies consisting only of super class and a single subclass. We also had plenty of references from other entities to either this sup or super classes. As you may expect this was a fertile ground for Hibernate HHH000179 warnings, and so I devoted a few hours of my time to figure out what this warning is all about. In our case providing proper equals() and hashCode() was all that was needed. But just to sum up I want to present the last, more real world example.

Shipped with framework X:

@Entity
@Table(name = "extensible_user")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "discriminator")
@DiscriminatorValue("NOT_USED")
public class LegacyUser {
    @Id
    @GeneratedValue
    private Long id;

    @Column
    private String userPreference1;

    @Column
    private String userPreference2;
    // ...
}

@Entity
@Table(name = "document")
public class LegacyDocument {
    @Id
    @GeneratedValue
    private Long id;

    @Column
    private String contents;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "owner_id")
    // !!! Entity referes to super class !!!
    private LegacyUser owner;

    // ...
}

Shipped with my module:

@Entity
@DiscriminatorValue("EXTENDED")
public class ExtendedUser extends LegacyUser {
    @Column
    private String userPreference3;
    // ...
}

@Entity
@Table(name = "comment")
public class Comment {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(/*...*/)
    @JoinColumn(name = "document_id")
    private LegacyDocument document;

    @ManyToOne(/*...*/
    @JoinColumn(name = "author_id")
    // !!! Entity refers to subclass !!!
    private ExtendedUser author;

    @Column
    private String contents;
    // ...
}

As you can see legacy class Document is using LegacyUser to refer to a system user. New class Comment is using ExtendedUser to refer to a system user.

Without proper equals() implementation we may get into troubles:

LegacyDocument document = 
    entityManager.find(LegacyDocument.class, documentId);

// we load some data from document owner
LegacyUser documentOwner = document.getOwner();
doSomethingWithOwner(documentOwner);

// HHH000179: Narrowing proxy to class ExtendedUser 
//  - this operation breaks ==
// When Hibernate loads comment that has 
// field of type ExtendedUser with the same Id as LegacyUser 
// it realizes that documentOwner is indeed ExtendedUser.
// So this time Hibernate could figure out that 
// it generated wrong proxy without querying DB.
List<Comment> comments = entityManager.createQuery(
            "select c from Comment c where c.document.id = :docId",
            Comment.class)
        .setParameter("docId", document.getId())
        .getResultList();

// Now the most interesting part
ExtendedUser commentAuthor = comments.get(0).getAuthor();

// comment author and doc author is the same user
assertThat(commentAuthor.getId())
        .isEqualTo(documentOwner.getId());

// but...
assertThat(commentAuthor)
        .isNotSameAs(documentOwner);

// Now without overloading hashCode()/equals() we may
// expect troubles...
Set<LegacyUser> users = new HashSet<>();
users.add(commentAuthor);
users.add(documentOwner);

assertThat(users).hasSize(2);

And that is all that I wanted to say about HHH000179. The most important thing that you should remember from this article is that with good equals() and hashCode() implementation HHH000179 warning can be safely ignored.

Source code: https://github.com/marcin-chwedczuk/hibernate_narrowing_proxy_warning_demo

marcin-chwedczuk

A Programmer, A Geek, A Human