Generally speaking, an ORM solution will have support for creating a lazy proxy for an entity
based on its identifier value. Historically Hibernate supported generating these proxies using
Java’s proxy feature (see java.lang.reflect.Proxy
). Hibernate now supports proxying an entity
using bytecode enhancement.
Example model
To discuss the different solutions to lazy loading, consider the following mapping:
@Entity
public class Person {
@Id
public Integer getId() {...}
String getName() {...}
@Lob
@Basic(fetch=LAZY)
String getSignature() {...}
...
}
and the following call:
Session session = ...;
Person p = session.load( Person.class, 1 );
which is the same as:
EntityManager em = ...;
Person p = em.getReference( Person.class, 1 );
Historical solution
As mentioned, historically Hibernate leveraged Java proxies to achieve this. On the
call to #load
in the example Hibernate would use java.lang.reflect.Proxy
to create
a proxy instance including just the id. In pseudo-code:
HibernateProxy proxy = new HibernateProxy(Person.class, 1);
The returned proxy extends Person
such that it can act like a Person to the application.
The proxy remains lazy until one of the non-id attributes (here name
or signature
is accessed, at which
point the proxy is initialized. Initializing the proxy triggers Hibernate to instantiate a Person
directly
and to associate it with the proxy. Again in pseudo-code:
Person target = new Person();
target.setId( 1 );
proxy.injectTarget( target );
At that point all calls to the proxy by the application are delegated to the "real" Person
instance.
Initial bytecode enhancement support
Say, for example, that signature
is expensive to load from the database (it is a LOB after all) - so
we want to load signature
only when it is accessed by the application. To support this use case,
Hibernate added limited support for bytecode enhancement allowing individual attributes of an entity to be
loaded later.
In this approach when a Person
is loaded Hibernate will:
-
instantiate a
Person
instance and inject the id -
select it’s non-lazy state (here
name
) from the database and inject it into the instantiatedPerson
Later, if Person#getSignature
is called, Hibernate will load signature
from the database
and inject it into the Person
instance.
Additionally, if the entity defined multiple lazy attributes, Hibernate allows the application to
group the attributes into a "lazy fetch group" (see @LazyGroup
) to be loaded together. Attributes
not explicitly assigned to a group are grouped together into an implicit group. When any not-yet-initialized
attribute is accessed, all of the attributes in the group to which that accessed attribute belongs are also
initialized.
Bytecode enhancement as proxy
You can think of this new feature as a combination of the above 2 approaches.
This new enhancement-as-proxy feature is considered a "tech preview" meaning it is currently supported on a
best effort basis. This feature must be enabled using the It uses the same enhancement code meaning entities do not need to be re-enhanced to move from the initial bytecode support to this new bytecode-as-proxy feature. It also works with lazy fetch groups. |
At a high-level, when the Person
is loaded Hibernate will:
-
instantiate a
Person
instance and inject the id
That is it. It does not load any database state at this point.
To describe how Hibernate initializes these proxies we need to first define some terms:
- baseline group
-
This group is defined by all of the entity’s non-lazy attributes.
- implicit group
-
All lazy attributes not explicitly assigned to a group
- explicit group
-
Lazy attributes explicitly associated with a group via
@LazyGroup
The implicit and explicit group distinction is the same as we discussed with regard to the legacy enhancement support.
What is new here is the baseline group. Whenever initialization of any part of the entity proxy is triggered we need to load this baseline group because it represents non-lazy state. Loading this baseline state happens in addition to loading the triggered lazy group.
For example, say we have:
Session session = ...;
Person p = session.load( Person.class, 1 );
String name = p.getName();
The call to p.getName()
here triggers initialization of the proxy. name
is non-lazy and therefore
belongs to the baseline group. It is loaded as soon as the proxy is initialized (also because it is the
attribute accessed, but ignore that for now).
However, because signature
is defined as lazy and because its lazy-group (the implicit group) is not accessed,
its data is not loaded from the database.
If instead we do:
Session session = ...;
Person p = session.load( Person.class, 1 );
String signature = p.getSignature();
Now both name
(as part of the baseline group) and signature
(part of the accessed lazy group) are loaded.
Specifically, both are loaded in the same SQL SELECT.
Now consider:
Session session = ...;
Person p = session.load( Person.class, 1 );
String name = p.getName();
String signature = p.getSignature();
Be aware that this triggers 2 different SQL SELECT statements - p.getName()
triggers loading of the baseline
state and then p.getSignature()
triggers loading the implicit group.
Limitations
The only real limitation is related to inheritance hierarchies. Specifically we do not create bytecode proxies for entities which have mapped subclasses. For these we fall back to using Java proxies. We need the indirection inherent in a proxy to allow for "narrowing" of the reference.
Consider a model with inheritance:
@Entity
@Inheritance
class Payment {
@Id
Integer getId() {...}
@Basic(fetch=LAZY)
MonetaryAmount getAmount() {...}
...
}
@Entity
class CardPayment extends Payment {
String getTransactionNumber();
...
}
@Entity
class CheckPayment extends Payment {
String getDriversLicenseNumber() {...}
...
}
and:
Session session = ...;
session.save( new CardPayment( 1, ... ) );
...
Session session = ...;
Payment p = session.load( Payment.class, 1 );
MonetaryAmount paidAmount = p.getAmount();
The application has asked Hibernate to load Payment
with an id of 1
. However, at this point, Hibernate has
no idea whether Payment#1
is a CardPayment
or a CheckPayment
. It will not know that until the proxy
is initialized.
The call to p.getAmount()
trigger initialization of the proxy at which point Hibernate knows that the Payment#1
reference actually refers to a CardPayment
. At this point Hibernate would need to instantiate a CardPayment
. The
problem with that is there is no way for Hibernate to "swap" the p
reference held by the application to be a
CardPayment
instead of the more general Payment
.
The Java proxy approach does not have this limitation. Because the Java proxy wraps the "real" entity Hibernate can delay the determination of the type of the entity until the proxy is initialized. The application still has a reference to the proxy and the proxy offers an indirection to the real entity.