The static method Hibernate.initialize() loads a lazy association from the database at once, while the EntityManager is still open. A lazy association is one that Hibernate does not read together with its owner. In its place, Hibernate puts a placeholder, which is either a proxy for a single entity or an uninitialized collection for a list or set.
The placeholder loads its data on first use, but only while the EntityManager is open. After the EntityManager is closed, the first use throws LazyInitializationException. We call Hibernate.initialize() before the transaction ends, so the data is ready for code that runs later, such as a view or a JSON mapper. For example, a REST endpoint returns a team with its players as JSON, and the JSON mapper reads the players after the transaction has ended.
The following example maps a lazy team on a player and lazy players on a team, and initializes both inside the transaction.
// Player
@ManyToOne(fetch = FetchType.LAZY) // the default for @ManyToOne is EAGER
@JoinColumn(name = "team_id")
private Team team;
// Team
@OneToMany(mappedBy = "team") // LAZY by default
private List<Player> players = new ArrayList<>();
Team lions = emf.callInTransaction(em -> {
Player lokesh = em.find(Player.class, lokeshId); // select ... from Player where id=?
Team team = lokesh.getTeam(); // proxy, no SQL
Hibernate.initialize(team); // select ... from Team where id=?
Hibernate.initialize(team.getPlayers()); // select ... from Player where team_id=?
return team;
});
String name = lions.getName(); // "Lions", after the transaction
List<Player> roster = lions.getPlayers(); // [Lokesh, Alex, Sara]
Notice that each Hibernate.initialize() call runs one SELECT, and the team and its players are still readable after the transaction.
Next, we compare the two kinds of placeholder, go through Hibernate.initialize() and its helper methods step by step, and look at the cost of calling it in a loop.
1. Proxies and Uninitialized Collections
When an entity has a lazy loading association, Hibernate reads only the owner’s row and fills the association with a placeholder object. The two kinds of placeholder work differently.
| Proxy | Uninitialized collection | |
|---|---|---|
| Used for | @ManyToOne(fetch = LAZY), em.getReference() | @OneToMany, @ManyToMany (both LAZY by default) |
| Class at runtime | Team$HibernateProxy, a generated subclass of Team | PersistentBag for our List |
| Knows before loading | The id, read from the foreign key | Its owner and the owner’s id |
| Loads on | Any getter except getId() | size(), a for loop over it |
| SQL to load | select … from Team where id=? | select … from Player where team_id=? |
Hibernate.initialize() accepts both kinds. It runs the same SELECT that the first getter call would run, and afterwards the placeholder answers every call from memory.

A getter call loads a placeholder too. A line such as team.getPlayers().size(); loads the collection, but a reader cannot tell that the call exists only to load data. Hibernate.initialize() states the purpose in its name and accepts any proxy or collection. It does nothing when the object is already loaded or null.
2. Hibernate.initialize() Example
Our example is a small football league with Hibernate 7.4, Java 25 and an in-memory H2 database. Three teams and six players are enough to see every query and to count them.
2.1. Teams and Players
A Team has many Player entities, and each Player points back to its Team through the team_id foreign key. The Team–Player mapping is a bidirectional one-to-many association, with both sides lazy as shown in the mapping at the top.
| Team | Players |
|---|---|
| Lions (id 1) | Lokesh, Alex, Sara |
| Tigers (id 2) | Maria, John |
| Eagles (id 3) | Emma |
The complete project on GitHub bootstraps Hibernate with HibernatePersistenceConfiguration, prints the SQL of every step and checks each result and statement count with 22 JUnit tests (mvn -q compile exec:java, mvn test).
2.2. Initializing a Proxy
The call em.find() loads the player and sets team to a proxy. Reading the id from the proxy runs no SQL, because the id came from the team_id column. Hibernate.isInitialized() tells us whether the data is there.
Player lokesh = em.find(Player.class, lokeshId); // select ... from Player where id=?
Team team = lokesh.getTeam();
String className = team.getClass().getSimpleName(); // "Team$HibernateProxy"
boolean loaded = Hibernate.isInitialized(team); // false
Long teamId = team.getId(); // 1, no SQL
boolean stillLoaded = Hibernate.isInitialized(team); // still false
Hibernate.initialize(team); // select t1_0.id,t1_0.name from Team t1_0 where t1_0.id=?
boolean loadedNow = Hibernate.isInitialized(team); // true
String name = team.getName(); // "Lions", no SQL
Hibernate.initialize(team); // no SQL, already loaded
A second call to Hibernate.initialize() on the same object runs no SQL, so it is safe to call it in shared code without checking first.
2.3. Initializing a Lazy Collection
The call em.find(Team.class, …) loads the team, and players is a PersistentBag with no elements yet. Hibernate.initialize() loads all players of the team with one query.
Team lions = em.find(Team.class, lionsId); // select ... from Team where id=?
List<Player> players = lions.getPlayers();
String className = players.getClass().getSimpleName(); // "PersistentBag"
boolean loaded = Hibernate.isInitialized(players); // false
Hibernate.initialize(players); // select p1_0.team_id,p1_0.id,p1_0.name from Player p1_0 where p1_0.team_id=?
boolean loadedNow = Hibernate.isInitialized(players); // true
String text = players.toString(); // "[Lokesh, Alex, Sara]"
2.4. Getting the Real Entity With Hibernate.unproxy()
An initialized proxy is still a Team$HibernateProxy, which forwards every call to the real Team object it loaded, but code that checks getClass() sees the proxy class. For example, an equals() method that compares getClass() values returns false for a proxy and a loaded Team with the same id. Hibernate has two methods that return the real class or the real object.
Hibernate.initialize(team);
String proxyClass = team.getClass().getSimpleName(); // "Team$HibernateProxy"
String realClass = Hibernate.getClass(team).getSimpleName(); // "Team"
Team real = Hibernate.unproxy(team, Team.class);
String unproxiedClass = real.getClass().getSimpleName(); // "Team"
boolean same = (real == team); // false, a different object
If the proxy is not loaded yet, Hibernate.unproxy() loads it first, with the same SELECT as Hibernate.initialize().
Team tigers = em.getReference(Team.class, tigersId); // proxy, no SQL
Team real = Hibernate.unproxy(tigers, Team.class); // select ... from Team where id=?
String name = real.getName(); // "Tigers"
2.5. Counting a Collection Without Loading It
Sometimes we need only the number of players, or whether one player is in the team. For example, a league page shows “3 players” next to each team name and never needs the player rows. Since Hibernate 6.1.1, the Hibernate class has methods that ask the database without loading the collection (isEmpty() was added in 7.0).
int size = Hibernate.size(lions.getPlayers()); // 3, select count(id) from Player where team_id=?
boolean hasAlex = Hibernate.contains(lions.getPlayers(), alex); // true, select 1 from Player where team_id=? and id=?
boolean loaded = Hibernate.isInitialized(lions.getPlayers()); // false, still not loaded
boolean empty = Hibernate.isEmpty(eagles.getPlayers()); // false, select count(id) from Player where team_id=?
| Method | SQL | Loads the collection? |
|---|---|---|
| Hibernate.initialize(players) | select … from Player where team_id=? | Yes |
| players.size() | select … from Player where team_id=? | Yes |
| Hibernate.size(players) | select count(id) from Player where team_id=? | No |
| Hibernate.contains(players, alex) | select 1 from Player where team_id=? and id=? | No |
| Hibernate.isEmpty(players) | select count(id) from Player where team_id=? | No |
For a team with thousands of players, Hibernate.size() reads one number instead of thousands of rows. The JavaDoc also lists Hibernate.get(), which reads one element of an indexed list or a map in the same way.
2.6. The Jakarta Persistence Way With PersistenceUnitUtil
Hibernate.initialize() is a Hibernate API. Jakarta Persistence has its own methods on PersistenceUnitUtil, which we get from the EntityManagerFactory. The method isLoaded() has been in the specification since JPA 2.0. Jakarta Persistence 3.2 added load(), the standard counterpart of Hibernate.initialize().
PersistenceUnitUtil util = emf.getPersistenceUnitUtil();
Player lokesh = em.find(Player.class, lokeshId);
boolean teamLoaded = util.isLoaded(lokesh, "team"); // false
boolean proxyLoaded = util.isLoaded(lokesh.getTeam()); // false
util.load(lokesh.getTeam()); // select t1_0.id,t1_0.name from Team t1_0 where t1_0.id=?
boolean teamLoadedNow = util.isLoaded(lokesh, "team"); // true
Collections behave differently in Hibernate 7.4.11. The call util.load(lions, “players”) ran no SQL for the lazy collection, and util.isLoaded(lions, “players”) stayed false until we called Hibernate.initialize(lions.getPlayers()). So for collections, we keep using Hibernate.initialize().
2.7. Initializing After the EntityManager Is Closed
A placeholder can load its data only while the persistence context that created it is open. The method emf.callInTransaction() closes the EntityManager when the lambda returns, and the returned entity becomes a detached entity. Calling Hibernate.initialize() on its proxy or collection fails with LazyInitializationException.
Player detached = emf.callInTransaction(em -> em.find(Player.class, lokeshId));
Hibernate.initialize(detached.getTeam()); // LazyInitializationException
Team detachedTeam = emf.callInTransaction(em -> em.find(Team.class, lionsId));
Hibernate.initialize(detachedTeam.getPlayers()); // LazyInitializationException
org.hibernate.LazyInitializationException: Could not initialize proxy [com.howtodoinjava.hibernate.initialize.Team#1] - no session
org.hibernate.LazyInitializationException: Cannot lazily initialize collection of role 'com.howtodoinjava.hibernate.initialize.Team.players' with key '1' (no session)
The fix is to move the Hibernate.initialize() call inside the transaction, as in the usage snippet at the top.

2.8. The Cost of Hibernate.initialize()
Each Hibernate.initialize() call runs its own SELECT. One extra SELECT is fine for one team, but in a loop over many teams it becomes the N+1 query problem, which means one query for the teams plus one query per team.
List<Team> teams = em.createQuery("from Team order by id", Team.class).getResultList();
teams.forEach(t -> Hibernate.initialize(t.getPlayers()));
// select t1_0.id,t1_0.name from Team t1_0 order by t1_0.id
// select ... from Player p1_0 where p1_0.team_id=? (Lions)
// select ... from Player p1_0 where p1_0.team_id=? (Tigers)
// select ... from Player p1_0 where p1_0.team_id=? (Eagles)
When we know in advance that we need the players of every team, we load them in the query that loads the teams. Three options do that.
- A left join fetch in the query loads teams and players in one SELECT.
- An entity graph names the associations to load and is passed as a query hint, so the query text stays the same.
- Batch fetching keeps the loop but loads the players of up to N teams with one IN query. We turn it on for all associations with the hibernate.default_batch_fetch_size setting, as in our example, or for one association with @BatchSize.
List<Team> teams = em.createQuery("select t from Team t left join fetch t.players order by t.id", Team.class)
.getResultList();
// select t1_0.id,t1_0.name,p1_0.team_id,p1_0.id,p1_0.name from Team t1_0
// left join Player p1_0 on t1_0.id=p1_0.team_id order by t1_0.id
EntityGraph<Team> graph = em.createEntityGraph(Team.class);
graph.addAttributeNode("players");
List<Team> teams = em.createQuery("from Team order by id", Team.class)
.setHint("jakarta.persistence.fetchgraph", graph)
.getResultList();
// same SQL as join fetch
List<Team> teams = em.createQuery("from Team order by id", Team.class).getResultList();
teams.forEach(t -> Hibernate.initialize(t.getPlayers()));
// select t1_0.id,t1_0.name from Team t1_0 order by t1_0.id
// select ... from Player p1_0 where p1_0.team_id in (?,?,?,?,?,?,?,?,?,?)
The statement counts from Hibernate statistics grow in different ways for 3 teams and for 100 teams with one player each.

We use Hibernate.initialize() for one entity or a few, and a join fetch or an entity graph when we load the association for a whole list. Batch fetching is a safety net for loops we cannot change.
3. Hibernate.initialize() FAQs
3.1. Should I Use hibernate.enable_lazy_load_no_trans Instead?
The hibernate.enable_lazy_load_no_trans setting lets a detached proxy or collection load itself after the EntityManager is closed. Hibernate opens a temporary session for each access.
Team detached = emf.callInTransaction(em -> em.find(Team.class, lionsId));
Hibernate.initialize(detached.getPlayers()); // works: select ... from Player where team_id=?
List<Player> players = detached.getPlayers(); // [Lokesh, Alex, Sara]
The Hibernate user guide marks this setting as unsafe and discourages it. Every access runs its own query outside our transaction, so a loop over detached teams creates the N+1 problem again, and each query can see data that another transaction changed in between. We keep it off and initialize what we need inside the transaction.
3.2. How Do I Initialize an Association of a Detached Entity?
We attach the entity to a new EntityManager first. em.merge() returns a managed copy whose collection can be loaded.
Team detached = emf.callInTransaction(em -> em.find(Team.class, lionsId));
emf.runInTransaction(em -> {
Team managed = em.merge(detached); // select ... from Team where id=?
Hibernate.initialize(managed.getPlayers()); // select ... from Player where team_id=?
});
In most applications, it is cleaner to load the team again with a join fetch query in the new transaction.
3.3. What Happens When the Proxy Points to a Missing Row?
The method em.getReference() creates a proxy without checking the database. Hibernate.initialize() then runs the SELECT and throws EntityNotFoundException when it finds no row.
Team missing = em.getReference(Team.class, 999L);
Hibernate.initialize(missing);
// jakarta.persistence.EntityNotFoundException: No row with the given identifier exists
// for entity [com.howtodoinjava.hibernate.initialize.Team with id '999']
3.4. What Changed Since the Old Session.get() Examples?
Older examples load the entity with session.get(), call Hibernate.initialize() and then session.close(). The idea is the same in Hibernate 7, with a few API changes.
| Old code | Hibernate 7 / Jakarta Persistence 3.2 |
|---|---|
| session.get(Team.class, id) (deprecated) | em.find(Team.class, id) |
| session.load(Team.class, id) (removed in Hibernate 7) | em.getReference(Team.class, id) |
| openSession() … session.close() | emf.callInTransaction(em -> …) closes the EntityManager for us |
| *javax.persistence.** | *jakarta.persistence.** |
| No standard way to load a proxy | emf.getPersistenceUnitUtil().load(proxy) |
| players.size() loads every row to count them | Hibernate.size(players) runs a count query |
The methods Hibernate.initialize() and Hibernate.isInitialized() work as before, and so does Hibernate.unproxy().
4. Conclusion
Hibernate.initialize() loads a lazy proxy or collection with one SELECT while the EntityManager is open, so the data stays usable after the transaction ends. Hibernate.isInitialized() checks the state, and Hibernate.unproxy() returns the real entity. The methods Hibernate.size() and contains() answer simple questions without loading the collection. For a whole list of entities, a join fetch or an entity graph loads the same data in one statement instead of one per entity.
5. References
- Hibernate class JavaDoc (Hibernate ORM 7.4)
- Hibernate ORM 7.4 User Guide: Fetching
- Hibernate ORM 7.4 User Guide: Batch fetching
- Hibernate ORM 7.4 User Guide: hibernate.enable_lazy_load_no_trans
- PersistenceUnitUtil JavaDoc (Jakarta Persistence 3.2)
- EntityGraph JavaDoc (Jakarta Persistence 3.2)
Happy Learning !!