How does "[requesting] that an association be eagerly fetched using a database join" "[protect us] from the infamous N+1 selects [problem]"?:
5.6. Proxies and lazy fetching
Our data model is a set of interconnected entities, and in Java our whole dataset would be represented as an enormous interconnected graph of objects. It’s possible that this graph is disconnected, but more likely it’s connected, or composed of a relatively small number of connected subgraphs.

Therefore, when we retrieve on object belonging to this graph from the database and instantiate it in memory, we simply can’t recursively retrieve and instantiate all its associated entities. Quite aside from the waste of memory on the VM side, this process would involve a huge number of round trips to the database server, or a massive multidimensional cartesian product of tables, or both. Instead, we’re forced to cut the graph somewhere.

Hibernate solves this problem using proxies and lazy fetching. A proxy is an object that masquerades as a real entity or collection, but doesn’t actually hold any state, because that state has not yet been fetched from the database. When you call a method of the proxy, Hibernate will detect the call and fetch the state from the database before allowing the invocation to proceed to the real entity object or collection.

Now for the gotchas:

Hibernate will only do this for an entity which is currently associated with a persistence context. Once the session ends, and the persistence context is cleaned up, the proxy is no longer fetchable, and instead its methods throw the hated LazyInitializationException.

A round trip to the database to fetch the state of a single entity instance is just about the least efficient way to access data. It almost inevitably leads to the infamous N+1 selects problem we’ll discuss later when we talk about how to optimize association fetching.

We’re getting a bit ahead of ourselves here, but let’s quickly mention the general strategy we recommend to navigate past these gotchas:

All associations should be set fetch=LAZY to avoid fetching extra data when it’s not needed. As we mentioned earlier, this setting is not the default for @ManyToOne associations, and must be specified explicitly.

But strive to avoid writing code which triggers lazy fetching. Instead, fetch all the data you’ll need upfront at the beginning of a unit of work, using one of the techniques described in Association fetching, usually, using join fetch in HQL or an EntityGraph.

It’s important to know that some operations which may be performed with an unfetched proxy don’t require fetching its state from the database. First, we’re always allowed to obtain its identifier:

var pubId = entityManager.find(Book.class, bookId).getPublisher().getId(); // does not fetch publisher
Second, we may create an association to a proxy:

book.setPublisher(entityManager.getReference(Publisher.class, pubId)); // does not fetch publisher
Sometimes it’s useful to test whether a proxy or collection has been fetched from the database. JPA lets us do this using the PersistenceUnitUtil:

boolean authorsFetched = entityManagerFactory.getPersistenceUnitUtil().isLoaded(book.getAuthors());
Hibernate has a slightly easier way to do it:

boolean authorsFetched = Hibernate.isInitialized(book.getAuthors());
But the static methods of the Hibernate class let us do a lot more, and it’s worth getting a bit familiar them.

Of particular interest are the operations which let us work with unfetched collections without fetching their state from the database. For example, consider this code:

Book book = session.find(Book.class, bookId);  // fetch just the Book, leaving authors unfetched
Author authorRef = session.getReference(Author.class, authorId);  // obtain an unfetched proxy
boolean isByAuthor = Hibernate.contains(book.getAuthors(), authorRef); // no fetching
This code fragment leaves both the set book.authors and the proxy authorRef unfetched.

Finally, Hibernate.initialize() is a convenience method that force-fetches a proxy or collection:

Book book = session.find(Book.class, bookId);  // fetch just the Book, leaving authors unfetched
Hibernate.initialize(book.getAuthors());  // fetch the Authors
But of course, this code is very inefficient, requiring two trips to the database to obtain data that could in principle be retrieved with just one query.

It’s clear from the discussion above that we need a way to request that an association be eagerly fetched using a database join, thus protecting ourselves from the infamous N+1 selects. One way to do that is by passing an EntityGraph to find().