How does a JPA Proxy work and how to unproxy it with Hibernate

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!


In this article, I’m going to explain how JPA and Hibernate Proxy objects work, and how you can unproxy an entity Proxy to get access to the underlying POJO instance.

The JPA lazy loading mechanism can either be implemented using Proxies or Bytecode Enhancement so that calls to lazy associations can be intercepted and relationships initialized prior to returning the result back to the caller.

Initially, in JPA 1.0, it was assumed that Proxies should not be a mandatory requirement, and that’s why @ManyToOne and @OneToOne associations use an EAGER loading strategy by default. However, EAGER fetching is bad for performance so it’s better to use the FetchType.LAZY fetching strategy for all association types.

In this article, we are going to see how the proxy mechanism works and how you can unproxy a given Proxy to the actual entity.

Loading a Proxy with JPA and Hibernate

The JPA EntityManager defines two ways to load a given entity.

When calling the find method, the entity is going to be loaded either from the first-level cache, second-level cache or from the database. Therefore, the returned entity is of the same type with the declared entity mapping.

On the contrary, when calling the getReference method, the returned object is a Proxy and not the actual entity object type. The benefit of returning a Proxy is that we can initialize a parent @ManyToOne or @OneToOne association without having to hit the database when we justs want to set a Foreign Key column with a value that we already know.

So, when running the following example:

Post post = entityManager.getReference(Post.class, 1L);

PostComment comment = new PostComment();
comment.setReview("A must read!");

Hibernate is going to issue a single INSERT statement without needing to execute any SELECT statement:

INSERT INTO post_comment (post_id, review, id) 
VALUES (1, 'A must read!', 1)

While this example underlines when Proxies are useful for writing data, Proxies are very convenient for reading data as well.

Considering we have the following PostComment entity:

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    private String review;

    //Getters and setters omitted for brevity

When executing the following test case:

PostComment comment = entityManager.find(
);"Loading the Post Proxy");

    "High-Performance Java Persistence",

Hibernate generates the following output:

SELECT AS id1_1_0_,
       pc.post_id AS post_id3_1_0_, AS review2_1_0_
FROM   post_comment pc

-- Loading the Post Proxy

SELECT AS id1_0_0_,
       p.title AS title2_0_0_
FROM   post p

The first SELECT statement fetches the PostComment entity without initializing the parent Post association since it was marked with FetchType.LAZY. By inspecting the selected FOREIGN KEY column, Hibernate knows whether to set the post association to null or to a Proxy. If the FOREIGN KEY column value is not null, then the Proxy will only populate the association identifier.

However, when accessing the title attribute, Hibernate needs to issue a secondary SELECT statement to initialize the Post Proxy.

How to unproxy a Proxy object with JPA and Hibernate

As we have already seen, by navigating the Proxy object, Hibernate issues the secondary SELECT statement and initializes the association. Hence the Proxy is replaced by the actual entity object.

Considering that the Post entity is mapped as follows:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    private Long id;

    private String title;

    //Getters and setters omitted for brevity

    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Post)) return false;
        return id != null && id.equals(((Post) o).getId());

    public int hashCode() {
        return getClass().hashCode();

When executing the following test case:

Post _post = doInJPA(entityManager -> {
    Post post = new Post();
    post.setTitle("High-Performance Java Persistence");
    return post;

doInJPA(entityManager -> {
    Post post = entityManager.getReference(Post.class, 1L);
        "Post entity class: {}", 



Hibernate generates the following output:

Post entity class:$Post_$$_jvst8fd_0

Because the Proxy object class is a dynamically generated type, so the Proxy post object is not equal to the _post object which is an actual Post class instance.

However, after calling the unproxy method, introduced in Hibernate 5.2.10, the original _post entity and the unproxied post object are equal.

Prior to Hibernate 5.2.10, to unproxy an object without traversing it, you’d have to execute the following logic:

Object unproxiedEntity = null;

if(proxy instanceof HibernateProxy) {
    HibernateProxy hibernateProxy = (HibernateProxy) proxy;
    LazyInitializer initializer = 
    unproxiedEntity = initializer.getImplementation();

Not very nice, right? Luckily, starting with Hibernate ORM 5.2.10, you can unproxy a Hibernate Proxy with the Hibernate#unproxy utility method:

Object unproxiedEntity = Hibernate.unproxy(proxy);

Much better!

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.


Understanding Hibernate internals can make a difference between an application that barely crawls and one that runs at warp speed. Lazy associations are very important from a performance perspective, and you really have to understand how Proxies work since you will inevitably bump into them on a daily basis.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.