diff --git a/documentation/src/main/asciidoc/introduction/Introduction.adoc b/documentation/src/main/asciidoc/introduction/Introduction.adoc index 25b7da6a59..30f152f565 100644 --- a/documentation/src/main/asciidoc/introduction/Introduction.adoc +++ b/documentation/src/main/asciidoc/introduction/Introduction.adoc @@ -437,23 +437,14 @@ We might start with something like this, a mix of UI and persistence logic: ---- @Path("/") @Produces("application/json") public class BookResource { - - @GET @Path("books/{titlePattern}") - public List findBooks(String titlePattern) { - var books = sessionFactory.fromTransaction(session -> { - return entityManager.createQuery("from Book where title like ?1 order by title", Book.class) - .setParameter(1, title) - .setMaxResults(max) - .setFirstResult(start) - .getResultList(); - }); - return books.isEmpty() ? Response.status(404).build() : books; + @GET @Path("book/{isbn}") + public Book getBook(String isbn) { + var book = sessionFactory.fromTransaction(session -> session.find(Book.class, isbn)); + return book == null ? Response.status(404).build() : book; } - } ---- - -Indeed, we might also _finish_ with something like that—it's quite hard to identify anything concretely wrong with the code above. +Indeed, we might also _finish_ with something like that—it's quite hard to identify anything concretely wrong with the code above, and for such a simple case it seems really difficult to justify making this code more complicated by introducing additional objects. One very nice aspect of this code, which we wish to draw your attention to, is that session and transaction management is handled by generic "framework" code, just as we already recommended above. In this case, we're using the `fromTransaction()` method, which happens to come built in to Hibernate. @@ -464,7 +455,31 @@ But you might prefer to use something else, for example: The important thing is that calls like `createEntityManager()` and `getTransaction().begin()` don't belong in regular program logic, because it's tricky and tedious to get the error handling correct. -There's one thing we could perhaps improve. +Let's now consider a slightly more complicated case. + +[source,java] +---- +@Path("/") @Produces("application/json") +public class BookResource { + private static final RESULTS_PER_PAGE = 20; + + @GET @Path("books/{titlePattern}/{page:\\d+}") + public List findBooks(String titlePattern, int page) { + var books = sessionFactory.fromTransaction(session -> { + return entityManager.createQuery("from Book where title like ?1 order by title", Book.class) + .setParameter(1, titlePattern) + .setMaxResults(RESULTS_PER_PAGE) // return at most 20 results + .setFirstResult(page*RESULTS_PER_PAGE) // start from the given page of results + .getResultList(); + }); + return books.isEmpty() ? Response.status(404).build() : books; + } + +} +---- + +This is fine, and we won't complain if you prefer to leave the code exactly as it appears above. +But there's one thing we could perhaps improve. We love super-short methods with single responsibilities, and there looks to be an opportunity to introduce one here. Let's hit the code with our favorite thing, the Extract Method refactoring. @@ -522,7 +537,8 @@ Whatever the case, the code which orchestrates a unit of work usually just calls @Path("books/{titlePattern}") public List findBooks(String titlePattern) { var books = sessionFactory.fromTransaction(session -> - Queries.findBooksByTitleWithPagination(session, titlePattern, 20, 20*page)); + Queries.findBooksByTitleWithPagination(session, titlePattern, + RESULTS_PER_PAGE, RESULTS_PER_PAGE*page)); return books.isEmpty() ? Response.status(404).build() : books; } ---- @@ -566,10 +582,8 @@ In an interface or abstract class, write down the "signature" of the query as a [source,java] ---- interface Queries { - @HQL("from Book where title like :title order by title offset :start fetch first :max rows only") List findBooksByTitleWithPagination(String title, int max, int start); - } ---- @@ -578,7 +592,9 @@ You can call the generated query method like this: [source,java] ---- -List books = Queries_.findBooksByTitleWithPagination(entityManager, titlePattern, 20, page*20); +List books = + Queries_.findBooksByTitleWithPagination(entityManager, titlePattern, + RESULTS_PER_PAGE, page*RESULTS_PER_PAGE); ---- A query method doesn't need to return `List`. @@ -587,10 +603,8 @@ It might return a single `Book`. [source,java] ---- interface Queries { - @HQL("from Book where isbn = :isbn") Book findBookByIsbn(String isbn); - } ---- @@ -599,10 +613,8 @@ It might even return `TypedQuery` or `SelectionQuery`: [source,java] ---- interface Queries { - @HQL("from Book where title like :title") SelectionQuery findBooksByTitle(String title); - } ---- @@ -612,9 +624,9 @@ This is extremely useful at times, since it allows the client to further manipul ---- List books = Queries_.findBooksByTitle(entityManager, titlePattern) - .ascending(Book_.title) // order the results - .setMaxResults(20) // return at most 20 results - .setFirstResult(page*20) // start from the given page of results + .ascending(Book_.title) // order the results + .setMaxResults(RESULTS_PER_PAGE) // return at most 20 results + .setFirstResult(page*RESULTS_PER_PAGE) // start from the given page of results .getResultList(); ---- @@ -622,6 +634,70 @@ List books = If we also set up the {query-validator}[Query Validator], our HQL query will even be completely _type-checked_ at compile time. +Now that we have a rough picture of what our persistence logic might look like, it's natural to ask how we should test this code. + +[[testing]] +=== Testing persistence logic + +:h2: http://www.h2database.com + +When we write tests for our persistence logic, we're going to need: + +1. a database, with +2. an instance of the schema mapped by our persistent entities, and +3. a set of test data, in a well-defined state at the beginning of each test. + +It might seem obvious that we should test against the same database system that we're going to use in production, and, indeed, we should certainly have at least _some_ tests for this configuration. +But on the other hand, tests which perform I/O are much slower than tests which don't, and most databases can't be set up to run in-process. + +So, since most persistence logic written using Hibernate 6 is _extremely_ portable between databases, it often makes good sense to test against an in-memory Java database. +({h2}[H2] is the one we recommend.) + +[CAUTION] +==== +We do need to be careful here if our persistence code uses native SQL, or if it uses concurrency-management features like pessimistic locks. +==== + +Whether we're testing against your real database, or against an in-memory Java database, we'll need to export the schema at the beginning of a test suite. +We _usually_ do this when we create the Hibernate `SessionFactory` or JPA `EntityManager`, and so traditionally we've used a <> for this. +The JPA-standard property is `jakarta.persistence.schema-generation.database.action`. + +Alternatively, in Hibernate 6, we may use the new `SchemaManager` API, just as we did <>. + +[source,java] +---- +sessionFactory.getSchemaManager().exportMappedObjects(true); +---- + +Since executing DDL statements is very slow on many databases, we don't want to do this before every test. +Instead, to ensure that each test begins with the test data in a well-defined state, we need to do two things before each test: + +1. clean up any mess left behind by the previous test, and then +2. reinitialize the test data. + +We may truncate all the tables, leaving an empty database schema, using the `SchemaManager`. + +[source,java] +---- +sessionFactory.getSchemaManager().truncateMappedObjects(); +---- + +After truncating tables, we might need to initialize our test data. +We may specify test data in a SQL script, for example: + +[source,sql] +./import.sql +---- +insert into Books (isbn, title) values ('9781932394153', 'Hibernate in Action') +insert into Books (isbn, title) values ('9781932394887', 'Java Persistence with Hibernate') +insert into Books (isbn, title) values ('9781617290459', 'Java Persistence with Hibernate, Second Edition') +---- + +If we name this file `import.sql`, and place it in the root classpath, that's all we need to do. +Otherwise, we need to specify the file in the <> `jakarta.persistence.schema-generation.create-script-source`. + +This SQL script will be executed every time `exportMappedObjects()` or `truncateMappedObjects()` is called. + [[architecture]] === Architecture and the persistence layer @@ -630,7 +706,7 @@ Let's now consider a different approach to code organization, one we treat with [WARNING] ==== In this section, we're going to give you our _opinion_. -If you're only interested in facts, or if you prefer not to read things that might cut against the opinion you currently hold, please feel free to skip straight to the <>. +If you're only interested in facts, or if you prefer not to read things that might undermine the opinion you currently hold, please feel free to skip straight to the <>. ==== Hibernate is an architecture-agnostic library, not a framework, and therefore integrates comfortably with a wide range of Java frameworks and containers. @@ -708,6 +784,7 @@ A layer of repository objects might make sense if you have multiple implementati That's because they're also extremely highly _coupled_ to their clients, with a very large API surface. A layer is only easily replaceable if it has a narrow API. +[%unbreakable] [TIP] ==== Some people do indeed use mock repositories for testing, but we really struggle to see any value in this.