Proxies

What a Proxy Is

For a LAZY @ManyToOne or getReferenceById(id), Hibernate does not load the row. It returns a proxy: a generated subclass of your entity (made with ByteBuddy, class name like Category$HibernateProxy$Xy12) that knows only the id. The first call to a non-id getter loads the row.

OrderItem item = em.find(OrderItem.class, 10L);
Product p = item.getProduct();            // proxy, no SQL yet
p.getId();                                // still no SQL: the id is known
Hibernate.isInitialized(p);               // false
p.getName();                              // select ... from products where id=?
Hibernate.isInitialized(p);               // true
Product real = Hibernate.unproxy(p, Product.class);   // the actual entity object

A Proxy in Plain Java

Example

Java's dynamic proxy shows the idea (Hibernate subclasses the entity, but the behaviour is the same): the id is free, other getters load the row once, and after the session closes loading fails:

import java.lang.reflect.*;

public class Main {
    static boolean sessionOpen = true;

    public static void main(String[] args) {
        Product p = lazyReference(7L);
        System.out.println("got reference, id=" + p.getId());
        System.out.println("name=" + p.getName());
        System.out.println("price=" + p.getPrice());

        Product q = lazyReference(8L);
        sessionOpen = false;                       // transaction ended
        try {
            q.getName();
        } catch (IllegalStateException e) {
            System.out.println("LazyInitializationException: " + e.getMessage());
        }
    }

    static Product lazyReference(long id) {
        InvocationHandler h = new InvocationHandler() {
            Product target;
            public Object invoke(Object proxy, Method m, Object[] a) throws Throwable {
                if (m.getName().equals("getId")) return id;
                if (target == null) {
                    if (!sessionOpen) throw new IllegalStateException("could not initialize proxy [Product#" + id + "] - no Session");
                    System.out.println("  select ... from products where id=" + id);
                    target = new RealProduct(id, "Prestige Cooker", 1899);
                }
                return m.invoke(target, a);
            }
        };
        return (Product) Proxy.newProxyInstance(Product.class.getClassLoader(), new Class<?>[]{Product.class}, h);
    }
}

interface Product { long getId(); String getName(); int getPrice(); }

record RealProduct(long id, String name, int price) implements Product {
    public long getId() { return id; }
    public String getName() { return name; }
    public int getPrice() { return price; }
}

Output

got reference, id=7
  select ... from products where id=7
name=Prestige Cooker
price=1899
LazyInitializationException: could not initialize proxy [Product#8] - no Session

Proxy Gotchas

  • p.getClass() is the proxy class, so getClass() == Product.class in equals fails. Use Hibernate.getClass(p) or instanceof.
  • Accessing fields directly (other.name) on a proxy reads the empty proxy field, not the data. Use getters in equals.
  • Entity classes and their getters must not be final, or Hibernate cannot subclass them (records cannot be entities for this reason).
  • A proxy of an inheritance root (Payment) is not an instance of the subclass (UpiPayment): instanceof UpiPayment is false until you unproxy.