create two new sections of the Intro to improve organization

This commit is contained in:
Gavin 2023-05-25 09:48:08 +02:00 committed by Christian Beikov
parent 7b009640c1
commit bdaa8fb582
3 changed files with 161 additions and 88 deletions

View File

@ -406,7 +406,7 @@ The JPA-standard API for this is a bit unwieldy:
----
var graph = entityManager.createEntityGraph(Book.class);
graph.addSubgraph(Book_.publisher);
entityManager.find(Book.class, bookId, Map.of(SpecHints.HINT_SPEC_FETCH_GRAPH, graph));
Book book = entityManager.find(Book.class, bookId, Map.of(SpecHints.HINT_SPEC_FETCH_GRAPH, graph));
----
This is untypesafe and unnecessarily verbose.
@ -416,7 +416,7 @@ Hibernate has a better way:
----
var graph = session.createEntityGraph(Book.class);
graph.addSubgraph(Book_.publisher);
session.byId(Book.class).withFetchGraph(graph).load(bookId);
Book book = session.byId(Book.class).withFetchGraph(graph).load(bookId);
----
This code adds a `left outer join` to our SQL query, fetching the associated `Publisher` along with the `Book`.
@ -428,7 +428,7 @@ We may even attach additional nodes to our `EntityGraph`:
var graph = session.createEntityGraph(Book.class);
graph.addSubgraph(Book_.publisher);
graph.addPluralSubgraph(Book_.authors).addSubgraph(Author_.person);
session.byId(Book.class).withFetchGraph(graph).load(bookId);
Book book = session.byId(Book.class).withFetchGraph(graph).load(bookId);
----
@ -566,7 +566,7 @@ List<Book> matchingBooks =
.getResultList();
----
The only difference between `createSelectionQuery()` and `createQuery()` is that `createSelectionQuery()` throw an exception if passed a Mutation.
The only difference between `createSelectionQuery()` and `createQuery()` is that `createSelectionQuery()` throw an exception if passed an `insert`, `delete`, or `update`.
In the query above, `:titleSearchPattern` is called a _named parameter_.
We may also identify parameters by a number.
@ -609,46 +609,7 @@ Book bookOrNull =
.getSingleResultOrNull();
----
The difference, of course, is that `getSingleResult()` throws an exception if there is no matching row in the database, whereas `getSingleResultOrNull()` just returns `null`.
[TIP]
====
Since Java has no tuple types, representing the type of a query projection list in Java has always been a problem for JPA and Hibernate.
Traditionally, we've just used `Object[]`:
[source,java]
----
var results =
session.createSelectionQuery("select isbn, title from Book",
Object[].class)
.getResultList();
for (var result : results) {
var isbn = (String) result[0];
var title = (String) result[1];
...
}
----
This is really a bit ugly.
Java's `record` types now offer an interesting alternative:
[source,java]
----
record IsbnTitle(String isbn, String title) {}
var results =
session.createSelectionQuery("select new IsbnTitle(isbn, title) from Book",
IsbnTitle.class)
.getResultList();
for (var result : results) {
var isbn = result.isbn();
var title = result.title();
...
}
----
Notice that we're able to declare the `record` right before the line which executes the query.
Now, this is only _superficially_ more typesafe, since the query itself is not checked statically, and so we can't say it's objectively better.
But perhaps you find it more aesthetically pleasing.
On the other hand, when we're passing query results around the system, the use of `select new` with a `record` type is much better than manually unpacking an `Object[]` array.
====
The difference, of course, is that `getSingleResult()` throws an exception if there's no matching row in the database, whereas `getSingleResultOrNull()` just returns `null`.
By default, Hibernate dirty checks entities in the persistence context before executing a query, in order to determine if the session should be flushed.
If there are many entities association with the persistence context, then this can be an expensive operation.
@ -880,6 +841,50 @@ List<Book> books =
.getResultList();
----
[[projection-lists]]
=== Representing projection lists
A _projection list_ is the list of things that a query returns, that is, the list of expressions in the `select` clause.
Since Java has no tuple types, representing query projection lists in Java has always been a problem for JPA and Hibernate.
Traditionally, we've just used `Object[]` most of the time:
[source,java]
----
var results =
session.createSelectionQuery("select isbn, title from Book", Object[].class)
.getResultList();
for (var result : results) {
var isbn = (String) result[0];
var title = (String) result[1];
...
}
----
This is really a bit ugly.
Java's `record` types now offer an interesting alternative:
[source,java]
----
record IsbnTitle(String isbn, String title) {}
var results =
session.createSelectionQuery("select new IsbnTitle(isbn, title) from Book", IsbnTitle.class)
.getResultList();
for (var result : results) {
var isbn = result.isbn();
var title = result.title();
...
}
----
Notice that we're able to declare the `record` right before the line which executes the query.
Now, this is only _superficially_ more typesafe, since the query itself is not checked statically, and so we can't say it's objectively better.
But perhaps you find it more aesthetically pleasing.
On the other hand, when we're passing query results around the system, the use of `select new` with a `record` type is much better than manually unpacking an `Object[]` array.
[[named-queries]]
=== Named queries
@ -899,8 +904,8 @@ We have to make sure that the class with the `@NamedQuery` annotation will be sc
- by adding `<class>org.hibernate.example.BookQueries</class>` to `persistence.xml`, or
- by calling `configuration.addClass(BookQueries.class)`.
The `@NamedNativeQuery` lets us do the same for native SQL queries.
There is much less advantage to using `@NamedNativeQuery`, because there is very little that Hibernate can do to validate the correctness of a query written in the native SQL dialect of your database.
The `@NamedNativeQuery` annotation lets us do the same for native SQL queries.
There's much less advantage to using `@NamedNativeQuery`, because there is very little that Hibernate can do to validate the correctness of a query written in the native SQL dialect of your database.
.Executing named queries
[cols="10,36,32,22"]
@ -923,6 +928,69 @@ List<Book> books =
Note that the code which executes the named query is not aware of whether the query was written in HQL or in native SQL, making it slightly easier to change and optimize the query later.
[[load-access]]
=== Controlling lookup by id
We can do almost anything via HQL, criteria, or native SQL queries.
But when we already know the identifier of the entity we need, a query can feel like overkill.
And queries don't make efficient use of the <<second-level-cache,second level cache>>.
We met the <<persistence-operations,`find()`>> method earlier.
It's the most basic way to perform a _lookup_ by id.
But as we also <<entity-graph,already saw>>, it can't quite do everything.
Therefore, Hibernate has some APIs that streamline certain more complicated lookups:
.Operations for lookup by id
[cols="30,~"]
|===
| Method name | Purpose
| `byId()` | Lets us specify association fetching via an `EntityGraph`, as we saw; also lets us specify some additional options, including how the lookup <<second-level-cache-management,interacts with the second level cache>>, and whether the entity should be loaded in read-only mode
| `byMultipleIds()` | Lets us load a _batch_ of ids at the same time
|===
Batch loading is very useful when we need to retrieve multiple instances of the same entity class by id:
[source,java]
----
var graph = session.createEntityGraph(Book.class);
graph.addSubgraph(Book_.publisher);
List<Book> books =
session.byMultipleIds(Book.class)
.withFetchGraph(graph) // control association fetching
.withBatchSize(20) // specify an explicit batch size
.with(CacheMode.GET) // control interaction with the cache
.multiLoad(bookIds);
----
The given list of `bookIds` will be broken into batches, and each batch will be fetched from the database in a single `select`.
If we don't specify the batch size explicitly, a batch size will be chosen automatically.
We also have some operations for working with lookups by <<natural-identifiers, natural id>>:
[cols="30,~"]
|===
| Method name | Purpose
| `bySimpleNaturalId()` | For an entity with just one attribute is annotated `@NaturalId`
| `byNaturalId()` | For an entity with multiple attributes are annotated `@NaturalId`
| `byMultipleNaturalId()` | Lets us load a _batch_ of natural ids at the same time
|===
Here's how we can retrieve an entity by its composite natural id:
[source,java]
----
Book book =
session.byNaturalId(Book.class)
.using(Book_.isbn, isbn)
.using(Book_.printing, printing)
.load();
----
Notice that this code fragment is completely typesafe, again thanks to the <<metamodel-generator,Metamodel Generator>>.
[[jdbc]]
=== Interacting directly with JDBC

View File

@ -36,10 +36,11 @@ The connection pool built in to Hibernate is suitable for testing, but isn't int
Instead, Hibernate supports a range of different connection pools, including our favorite, Agroal.
To select and configure Agroal, you'll need to set some extra configuration properties, in addition to the settings we already saw in <<basic-configuration-settings>>.
For example:
Properties with the prefix `hibernate.agroal` are passed through to Agroal:
[source,properties]
----
# configure Agroal connection pool
hibernate.agroal.maxSize 20
hibernate.agroal.minSize 10
hibernate.agroal.acquisitionTimeout PT1s
@ -47,6 +48,7 @@ hibernate.agroal.reapTimeout PT10s
----
As long as you set at least one property with the prefix `hibernate.agroal`, the `AgroalConnectionProvider` will be selected automatically.
There's many to choose from:
.Settings for configuring Agroal
[cols="37,~"]
@ -63,6 +65,13 @@ As long as you set at least one property with the prefix `hibernate.agroal`, the
| `hibernate.agroal.idleValidationTimeout` | A foreground validation is executed if a connection has been idle on the pool for longer than this duration
| `hibernate.agroal.validationTimeout` | The interval between background validation checks
| `hibernate.agroal.initialSql` | A SQL command to be executed when a connection is created
|===
The following settings are common to all connection pools supported by Hibernate:
.Common settings for connection pools
[cols="37,~"]
|===
| `hibernate.connection.autocommit` | The default autocommit mode
| `hibernate.connection.isolation` | The default transaction isolation level
|===
@ -358,23 +367,7 @@ class Book {
}
----
This cache is utilized when the entity is retrieved using one of the operations of `Session` which performs lookup by natural id:
- `bySimpleNaturalId()` if just one attribute is annotation `@NaturalId`, or
- `byNaturalId()` if multiple attributes are annotated `@NaturalId`.
Here's how we can retrieve an entity by its composite natural id:
[source,java]
----
Book book =
session.byNaturalId(Book.class)
.using(Book_.isbn, isbn)
.using(Book_.printing, printing)
.load();
----
Notice that this code fragment is completely typesafe.
This cache is utilized when the entity is retrieved using one of the operations of `Session` which performs <<load-access,lookup by natural id>>.
[NOTE]
====
@ -589,37 +582,30 @@ entityManager.setCacheRetrieveMode(CacheRetrieveMode.BYPASS);
entityManager.setCacheStoreMode(CacheStoreMode.BYPASS);
----
The JPA-defined cache modes are:
The JPA-defined cache modes come in two flavors: `CacheRetrieveMode` and `CacheStoreMode`.
.JPA-defined cache modes
.JPA-defined cache retrieval modes
[cols="30,~"]
|===
| Mode | Interpretation
| `CacheRetrieveMode.USE` | Read data from the cache if available
| `CacheRetrieveMode.BYPASS` | Don't read data from the cache; go direct to the database
|===
We might select `CacheRetrieveMode.BYPASS` if we're concerned about the possibility of reading stale data from the cache.
.JPA-defined cache storage modes
[cols="30,~"]
|===
| Mode | Interpretation
| `CacheStoreMode.USE` | Write data to the cache when read from the database or when modified; do not update already-cached items when reading
| `CacheStoreMode.REFRESH` | Write data to the cache when read from the database or when modified; always update cached items when reading
| `CacheStoreMode.BYPASS` | Don't write data to the cache
|===
A Hibernate `CacheMode` packages a `CacheRetrieveMode` with a `CacheStoreMode`.
.Hibernate cache modes and JPA equivalents
[cols="30,~"]
|===
| Hibernate `CacheMode` | Equivalent JPA modes
| `NORMAL` | `CacheRetrieveMode.USE`, `CacheStoreMode.USE`
| `IGNORE` | `CacheRetrieveMode.BYPASS`, `CacheStoreMode.BYPASS`
| `GET` | `CacheRetrieveMode.USE`, `CacheStoreMode.BYPASS`
| `PUT` | `CacheRetrieveMode.BYPASS`, `CacheStoreMode.USE`
| `REFRESH` | `CacheRetrieveMode.REFRESH`, `CacheStoreMode.BYPASS`
|===
There's no particular reason to prefer Hibernate's `CacheMode` to the JPA equivalents.
This enumeration only exists because Hibernate had cache modes long before they were added to JPA.
We might select `CacheStoreMode.BYPASS` if we're querying data that we don't expect to use again soon.
[TIP]
// .A good time to `BYPASS` the cache
@ -649,6 +635,23 @@ List<Publisher> allpubs =
----
====
A Hibernate `CacheMode` packages a `CacheRetrieveMode` with a `CacheStoreMode`.
.Hibernate cache modes and JPA equivalents
[cols="30,~"]
|===
| Hibernate `CacheMode` | Equivalent JPA modes
| `NORMAL` | `CacheRetrieveMode.USE`, `CacheStoreMode.USE`
| `IGNORE` | `CacheRetrieveMode.BYPASS`, `CacheStoreMode.BYPASS`
| `GET` | `CacheRetrieveMode.USE`, `CacheStoreMode.BYPASS`
| `PUT` | `CacheRetrieveMode.BYPASS`, `CacheStoreMode.USE`
| `REFRESH` | `CacheRetrieveMode.REFRESH`, `CacheStoreMode.BYPASS`
|===
There's no particular reason to prefer Hibernate's `CacheMode` over the JPA equivalents.
This enumeration only exists because Hibernate had cache modes long before they were added to JPA.
[TIP]
====
For "reference" data, that is, for data which is expected to always be found in the second-level cache, it's a good idea to _prime_ the cache at startup.
@ -657,7 +660,9 @@ There's a really easy way to do this: just execute a query immediately after obt
[source,java]
----
SessionFactory sessionFactory = setupHibernate(new Configuration()).buildSessionFactory();
SessionFactory sessionFactory =
setupHibernate(new Configuration())
.buildSessionFactory();
// prime the second-level cache
sessionFactory.inSession(session -> {
session.createSelectionQuery("from Countries"))

View File

@ -46,12 +46,12 @@ image:
align: center
codespan:
font:
size: 8.5
size: 0.94em
family: Inconsolata Light
color: #281e5d
code:
font:
size: 8.5
size: 0.94em
color: #281e5d
family: Inconsolata Light
border-width: 0
@ -83,11 +83,11 @@ list:
indent: $base-font-size * 1.5
item-spacing: 2
table:
font-size: 8.5
font-size: 0.94em
caption:
text-align: right
text-align: center
side: top
font-size: 8
font-size: 0.9em
grid:
color: #f0f0f0
style: solid