From 69e0809bf496272037488401c70b8d39df4e1c31 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 11 Jul 2023 23:40:08 +0200 Subject: [PATCH] new documentation chapter for @HQL, @SQL, and @Find methods --- .../main/asciidoc/introduction/Generator.adoc | 526 ++++++++++++++++++ .../introduction/Hibernate_Introduction.adoc | 18 +- .../asciidoc/introduction/Introduction.adoc | 162 ++---- 3 files changed, 564 insertions(+), 142 deletions(-) create mode 100644 documentation/src/main/asciidoc/introduction/Generator.adoc diff --git a/documentation/src/main/asciidoc/introduction/Generator.adoc b/documentation/src/main/asciidoc/introduction/Generator.adoc new file mode 100644 index 0000000000..42ae8abef1 --- /dev/null +++ b/documentation/src/main/asciidoc/introduction/Generator.adoc @@ -0,0 +1,526 @@ +[[generator]] +== Compile-time tooling + +The Metamodel Generator is a standard part of JPA. +// It's an annotation processor that produces a statically-typed metamodel of the entity classes in a Java program. +We've actually already seen its handiwork in the code examples <>: it's the author of the class `Book_`, which contains the static metamodel of the <> `Book`. + +[[metamodel-generator]] +.The Metamodel Generator +**** + +:generator: https://hibernate.org/orm/tooling/ +:generator-guide: {userGuideBase}#tooling-modelgen + +Hibernate's {generator}[Metamodel Generator] is an annotation processor that produces what JPA calls a _static metamodel_. +That is, it produces a typed model of the persistent classes in our program, giving us a type-safe way to refer to their attributes in Java code. +In particular, it lets us specify <> and <> in a completely type-safe way. + +The history behind this thing is quite interesting. +Back when Java's annotation processing API was brand spankin' new, the static metamodel for JPA was proposed by Gavin King for inclusion in JPA 2.0, as a way to achieve type safety in the nascent criteria query API. +It's fair to say that, back in 2010, this API was not a runaway success. +Tools did not, at the time, feature robust support for annotation processors. +And all the explicit generic types made user code quite verbose and difficult to read. +(The need for an explicit reference to a `CriteriaBuilder` instance also contributed verbosity to the criteria API.) +For years, Gavin counted this as one of his more embarrassing missteps. + +But time has been kind to the static metamodel. +In 2023, all Java compilers, build tools, and IDEs have robust support for annotation processing, and Java's local type inference (the `var` keyword) eliminates the verbose generic types. +JPA's `CriteriaBuilder` and `EntityGraph` APIs are still not quite perfect, but the imperfections aren't related to static type safety or annotation processing. +The static metamodel itself is undeniably useful and elegant. + +And so now, in Hibernate 6.3, we're finally ready to go new places with the Metamodel Generator. +And it turns out that there's quite a lot of unlocked potential there. + +Now, you still don't have to use the Metamodel Generator with Hibernate—the APIs we just mentioned still also accept plain strings—but we find that it works well with Gradle and integrates smoothly with our IDE, and the advantage in type-safety is compelling. +**** + +[TIP] +==== +We've already seen how to set up the annotation processor in the <> we saw earlier. +==== + +Here's an example of the sort of code that's generated for an entity class, as mandated by the JPA specification: + +[source,java] +.Generated Code +---- +@StaticMetamodel(Book.class) +public abstract class Book_ { + + /** + * @see org.example.Book#isbn + **/ + public static volatile SingularAttribute isbn; + + /** + * @see org.example.Book#text + **/ + public static volatile SingularAttribute text; + + /** + * @see org.example.Book#title + **/ + public static volatile SingularAttribute title; + + /** + * @see org.example.Book#type + **/ + public static volatile SingularAttribute type; + + /** + * @see org.example.Book#publicationDate + **/ + public static volatile SingularAttribute publicationDate; + + /** + * @see org.example.Book#publisher + **/ + public static volatile SingularAttribute publisher; + + /** + * @see org.example.Book#authors + **/ + public static volatile SetAttribute authors; + + public static final String ISBN = "isbn"; + public static final String TEXT = "text"; + public static final String TITLE = "title"; + public static final String TYPE = "type"; + public static final String PUBLICATION_DATE = "publicationDate"; + public static final String PUBLISHER = "publisher"; + public static final String AUTHORS = "authors"; + +} +---- + +We've already been using metamodel references like `Book_.authors` and `Book.AUTHORS` in the previous chapters. +So now lets see what else the Metamodel Generator can do for us. + +Automatic generation of _finder methods_ and _query methods_ is a new feature of Hibernate's implementation of the Metamodel Generator, and an extension to the functionality defined by the JPA specification. +In this chapter, we're going to explore these features. + +To whet our appetites, let's see how it works for a `@NamedQuery`. + +[[generated-named-queries]] +=== Named queries and the Metamodel Generator + +The very simplest way to generate a query method is to put a `@NamedQuery` annotation anywhere we like, with a `name` beginning with the magical character `#`. + +Let's just stick it on the `Book` class: + +[source,java] +---- +@CheckHQL // validate the query at compile time +@NamedQuery(name = "#findByTitleAndType", + query = "select book from Book book " + + "where book.title like :titlePattern " + + " and book.type = :type") +@Entity +public class Book { ... } +---- + +Now the Metamodel Generator adds the following method declaration to the metamodel class `Book_`. + +[source,java] +.Generated Code +---- +/** + * Executes named query {@value #QUERY_FIND_BY_TITLE_AND_TYPE} defined by annotation of {@link Book}. + **/ +public static List findByTitleAndType(EntityManager entityManager, String titlePattern, Type type) { + return entityManager.createNamedQuery(QUERY_FIND_BY_TITLE_AND_TYPE) + .setParameter("titlePattern", titlePattern) + .setParameter("type", type) + .getResultList(); +} +---- + +We can easily call this method from wherever we like, as long as we have access to an `EntityManager`: + +[source,java] +---- +List books = + Book_.findByTitleAndType(entityManager, titlePattern, Type.BOOK); +---- + +Now, this is quite nice, but it's a bit inflexible in various ways, and so this probably _isn't_ the best way to generate a query method. + +[[generated-query-methods]] +=== Generated query methods + +The problem with generating the query method straight from the `@NamedQuery` annotation is that it doesn't let us explicitly specify the return type or parameter list. +The Metamodel Generator does a good job of inferring the query return type and parameter types, but we're often going to need a bit more control. + +The solution is to write down the signature of the query method _explicitly_, as an abstract method in Java. +We'll need a place to put this method, and since our `Book` entity isn't an abstract class, we'll just introduce a new interface for this purpose: + +[source,java] +---- +interface Queries { + @HQL("from Book where title like :title and type = :type") + List findBooksByTitleAndType(String title, String type); +} +---- + +Instead of `@NamedQuery`, which is a type-level annotation, we specify the HQL query using the new `@HQL` annotation, which we place directly on the query method. + +This results in the following generated code in the `Queries_` class: + +[source,java] +.Generated Code +---- +@StaticMetamodel(Queries.class) +public abstract class Queries_ { + + /** + * @see org.example.Queries#findBooksByTitleAndType(String,Type) + **/ + public static List findBooksByTitleAndType(@Nonnull EntityManager entityManager, String title, Type type) { + return entityManager.createQuery(FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type, Book.class) + .setParameter("title", title) + .setParameter("type", type) + .getResultList(); + } + + static final String FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type = + "from Book where title like :title and type = :type"; + +} + +---- + +Notice that the signature differs just slightly from the one we wrote down in the `Queries` interface: the Metamodel Generator has prepended a parameter accepting `EntityManager` to the parameter list. + +The Metamodel Generator verifies that the parameters of our abstract method declaration match the parameters of the HQL query, for example: + +- for a named parameter `:alice`, there must be a method parameter named `alice` with exactly the same type, or +- for an ordinal parameter `?2`, the second method parameter must have exactly the same type. + +The query must also be syntactically legal and semantically well-typed, that is, the entities, attributes, and functions referenced in the query must actually exist and have compatible types. +The Metamodel Generator determines this by inspecting the annotations of the entity classes at compile time. + +[NOTE] +==== +The `@CheckHQL` annotation which instructs Hibernate to validate named queries is _not_ necessary for query methods annotated `@HQL`. +==== + +The `@HQL` annotation has a friend named `@SQL` which lets us specify a query written in native SQL instead of in HQL. +In this case there's a lot less the Metamodel Generator can do to check that the query is legal and well-typed. + +We imagine you're wondering whether a `static` method is really the right thing to use here. + +[[static-or-instance]] +=== Generating query methods as instance methods + +One thing not to like about what we've just seen is that we can't transparently replace a generated `static` function of the `Queries_` class with an improved handwritten implementation without impacting clients. +Now, if our query is only called in one place, which is quite common, this isn't going to be a big issue, and so we're inclined to think the `static` function is fine. + +But if this function is called from many places, it's probably better to promote it to an instance method of some class or interface. +Fortunately, this is straightforward. + +All we need to do is add an abstract getter method for the session object to our `Queries` interface. +We may call this method anything we like: + +[source,java] +---- +interface Queries { + EntityManager entityManager(); + + @HQL("from Book where title like :title and type = :type") + List findBooksByTitleAndType(String title, String type); +} +---- + +Here we've used `EntityManager` as the session type, but other types are allowed: + +- `Session`, +- `StatelessSession`, or +- `Mutiny.Session` from Hibernate Reactive. + +Now the Metamodel Generator does something a bit different: + +[source,java] +.Generated Code +---- +@StaticMetamodel(Queries.class) +public class Queries_ implements Queries { + + private final @Nonnull EntityManager entityManager; + + public Queries_(@Nonnull EntityManager entityManager) { + this.entityManager = entityManager; + } + + public @Nonnull EntityManager entityManager() { + return entityManager; + } + + /** + * @see org.example.Queries#findBooksByTitleAndType(String,Type) + **/ + @Override + public List findBooksByTitleAndType(String title, Type type) { + return entityManager.createQuery(FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type, Book.class) + .setParameter("title", title) + .setParameter("type", type) + .getResultList(); + } + + static final String FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type = + "from Book where title like :title and type = :type"; + +} +---- + +The generated class `Queries_` now implements the `Queries` interface, and the generated query method implements our abstract method directly. + +Of course, the protocol for calling the query method has to change: + +[source,java] +---- +Queries queries = new Queries_(entityManager); +List books = queries.findByTitleAndType(titlePattern, Type.BOOK); +---- + +If we ever need to swap out the generated query method with one we write by hand, without impacting clients, all we need to do is replace the abstract method with a `default` method of the `Queries` interface. + +[TIP] +==== +As you recall, we don't think these things really need to be container-managed objects. +But if you _want_ them to be—if you're allergic to calling constructors, for some reason—then: + +- placing `jakarta.inject` on the build path will cause an `@Inject` annotation to be added to the constructor of `Queries_`, and +- placing `jakarta.enterprise.context` on the build path will cause a `@Dependent` annotation to be added to the `Queries_` class. + +Thus, the generated implementation of `Queries` will be a perfectly functional CDI bean with no extra work to be done. +==== + +Is the `Queries` interface starting to look a lot like a DAO-style repository object? +Well, perhaps. +You can certainly _decide to use_ this facility to create a `BookRepository` if that's what you prefer. +But unlike a repository, our `Queries` interface: + +- doesn't attempt to hide the `EntityManager` from its clients, +- doesn't implement or extend any framework-provided interface or abstract class, at least not unless you want to create such a framework yourself, and +- isn't restricted to service a particular entity class. + +We can have as many or as few interfaces with query methods as we like. +There's no one-one-correspondence between these interfaces and entity types. +This approach is so flexible that we don't even really know what to call these "interfaces with query methods". + +[[generated-finder-methods]] +=== Generated finder methods + +At this point, one usually begins to question whether it's even necessary to write a query at all. +Would it be possible to just infer the query from the method signature? + +In some simple cases it's indeed possible, and this is the purpose of _finder methods_. +A finder method is a method annotated `@Find`. +For example: + +[source,java] +---- +@Find +Book getBook(String isbn); +---- + +A finder method may have multiple parameters: + +[source,java] +---- +@Find +List getBooksByTitle(String title, Type type); +---- + +The name of the finder method is arbitrary and carries no semantics. +But: + +- the return type determines the entity class to be queried, and +- the parameters of the method must match the fields of the entity class _exactly_, by both name and type. + +Considering our first example, `Book` has a persistent field `String isbn`, so this finder method is legal. +If there were no field named `isbn` in `Book`, or if it had a different type, this method declaration would be rejected with a meaningful error at compile time. +Similarly, the second example is legal, since `Book` has fields `String title` and `Type type`. + +[IMPORTANT] +==== +You might notice that our solution to this problem is very different from the approach taken by others. +In DAO-style repository frameworks, you're asked to encode the semantics of the finder method into the _name of the method_. +This idea came to Java from Ruby, and we think it doesn't belong here. +It's completely unnatural in Java, and by almost any measure other than _counting characters_ it's objectively worse than just writing the query in a string literal. +At least string literals accommodate whitespace and punctuation characters. +Oh and, you know, it's pretty useful to be able to rename a finder method _without changing its semantics_. 🙄 +==== + +The code generated for this finder method depends on what kind of fields match the method parameters: + +[cols="35,~"] +|=== +|`@Id` field | Uses `EntityManager.find()` +|All `@NaturalId` fields | Uses `Session.byNaturalId()` +|Other persistent fields, or a mix of field types | Uses a criteria query +|=== + +The generated code also depends on what kind of session we have, since the capabilities of stateless sessions, and of reactive sessions, differ slightly from the capabilities of regular stateful sessions. + +A finder method may specify <>, for example: + +[source,java] +---- +@Find(namedFetchProfiles=Book_.FETCH_WITH_AUTHORS) +Book getBookWithAuthors(String isbn); +---- + +This lets us declare which associations of `Book` should be pre-fetched by annotating the `Book` class. + + +// In an interface or abstract class, write down the "signature" of the query as a function, and specify the HQL or SQL query string itself using a `@HQL` or `@SQL` annotation: +// +// [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); +// } +// ---- +// +// [TIP] +// ==== +// :query-validator: https://github.com/hibernate/query-validator/ +// +// Don't like putting queries in annotations? +// No problem. +// If we set up the {query-validator}[Query Validator], we get validation of queries passed as strings directly to the Hibernate session. +// ==== +// +// A query method with a similar signature and return type is generated in the corresponding static metamodel class `Queries_`. +// We can call the generated query method like this: +// +// [source,java] +// ---- +// List books = +// Queries_.findBooksByTitleWithPagination(entityManager, titlePattern, +// RESULTS_PER_PAGE, page*RESULTS_PER_PAGE); +// ---- + +[[paging-and-ordering]] +=== Paging and ordering + +Optionally, a query method may have additional "magic" parameters which do not map to query parameters: + +[cols="19,~,32m"] +|=== +| Parameter type | Purpose | Example argument + +| `Page` | Specifies a page of query results | Page.first(20) +| `Order` | Specifies an entity attribute to order by, if `E` is the entity type returned by the query | Order.asc(Book_.title) +| `List` + +(or varargs) | Specifies entity attributes to order by, if `E` is the entity type returned by the query | List.of(Order.asc(Book_.title), Order.asc(Book_.isbn)) +| `Order` | Specifies a column to order by, if the query returns a projection list | Order.asc(1) +| `List` + +(or varargs) | Specifies columns to order by, if the query returns a projection list | List.of(Order.asc(1), Order.desc(2)) +|=== + +Thus, if we redefine our earlier query method as follows: + +[source,java] +---- +interface Queries { + @HQL("from Book where title like :title and type = :type") + List findBooksByTitleAndType(String title, Page page, Order... order); +} +---- + +Then we can call it like this: + +[source,java] +---- +List books = + Queries_.findBooksByTitleAndType(entityManager, titlePattern, Type.BOOK, + Page.page(RESULTS_PER_PAGE, page), Order.asc(Book_.isbn)); +---- + + +[[return-types]] +=== Query and finder method return types + +A query method doesn't need to return `List`. +It might return a single `Book`. + +[source,java] +---- +@HQL("from Book where isbn = :isbn") +Book findBookByIsbn(String isbn); +---- + +For a query with a projection list, `Object[]` or `List` is permitted: + +[source,java] +---- +@HQL("select isbn, title from Book where isbn = :isbn") +Object[] findBookAttributesByIsbn(String isbn); +---- + +But when there's just one item in the `select` list, the type of that item should be used: + +[source,java] +---- +@HQL("select title from Book where isbn = :isbn") +String getBookTitleByIsbn(String isbn); +---- + +A query method might even return `TypedQuery` or `SelectionQuery`: + +[source,java] +---- +@HQL("from Book where title like :title") +SelectionQuery findBooksByTitle(String title); +---- + +This is extremely useful at times, since it allows the client to further manipulate the query: + +[source,java] +---- +List books = + Queries_.findBooksByTitle(entityManager, titlePattern) + .setOrder(Order.asc(Book_.title)) // order the results + .setPage(Page.page(RESULTS_PER_PAGE, page)) // return the given page of results + .setFlushMode(FlushModeType.COMMIT) // don't flush session before query execution + .setReadOnly(true) // load the entities in read-only mode + .setCacheStoreMode(CacheStoreMode.BYPASS) // don't cache the results + .setComment("Hello world!") // add a comment to the generated SQL + .getResultList(); +---- + +Finally, a query method might return a `Pager`. +This is an incubating API in Hibernate 6.3 that makes it easy to paginate query result sets. +A query method returning type `Pager` must accept a `Page` object specifying the initial page. + +[source,java] +---- +@HQL("from Book where title like :title") +Pager findBooksByTitle(String title, Page initialPage); +---- + +There are several idioms for the use of `Pager`, here's one: + +[source,java] +---- +new Queries_(session) + .findBooksByTitle(title, Page.first(pageSize)) + .forEachRemainingPage(books -> { + for (Book book : books) { + ... + } + session.clear(); + }) +---- + +On the other hand, finder methods are currently much more limited. +A finder method must return an entity type like `Book`, or a list of the entity type, `List`, for example. + +[NOTE] +==== +As you might expect, for a reactive session, all query methods and finder methods must return `Uni`. +==== \ No newline at end of file diff --git a/documentation/src/main/asciidoc/introduction/Hibernate_Introduction.adoc b/documentation/src/main/asciidoc/introduction/Hibernate_Introduction.adoc index 7db0434330..ad937ee071 100644 --- a/documentation/src/main/asciidoc/introduction/Hibernate_Introduction.adoc +++ b/documentation/src/main/asciidoc/introduction/Hibernate_Introduction.adoc @@ -13,39 +13,25 @@ Gavin King and the Hibernate team :toc: :toclevels: 3 -<<< - include::Preface.adoc[] -<<< - :numbered: include::Introduction.adoc[] -<<< - include::Configuration.adoc[] -<<< - include::Entities.adoc[] -<<< - include::Mapping.adoc[] -<<< - include::Interacting.adoc[] -// include::../userguide/chapters/query/hql/Hibernate_Query_Language.adoc[] +include::Generator.adoc[] -<<< +// include::../userguide/chapters/query/hql/Hibernate_Query_Language.adoc[] include::Tuning.adoc[] -<<< - include::Advanced.adoc[] diff --git a/documentation/src/main/asciidoc/introduction/Introduction.adoc b/documentation/src/main/asciidoc/introduction/Introduction.adoc index c94f784739..10382a5b75 100644 --- a/documentation/src/main/asciidoc/introduction/Introduction.adoc +++ b/documentation/src/main/asciidoc/introduction/Introduction.adoc @@ -469,8 +469,7 @@ public class BookResource { 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 + .setPage(Page.page(RESULTS_PER_PAGE, page)) .getResultList(); }); return books.isEmpty() ? Response.status(404).build() : books; @@ -487,11 +486,10 @@ Let's hit the code with our favorite thing, the Extract Method refactoring. We o [source,java] ---- static List findBooksByTitleWithPagination(EntityManager entityManager, - String titlePattern, int max, int start) { + String titlePattern, Page page) { return entityManager.createQuery("from Book where title like ?1 order by title", Book.class) .setParameter(1, titlePattern) - .setMaxResults(max) - .setFirstResult(start) + .setPage(page) .getResultList(); } ---- @@ -512,11 +510,10 @@ We need a place to put the annotation, so lets move our query method to a new cl class Queries { static List findBooksByTitleWithPagination(EntityManager entityManager, - String titlePattern, int max, int start) { + String titlePattern, Page page) { return entityManager.createNamedQuery("findBooksByTitle", Book.class) .setParameter("title", titlePattern) - .setMaxResults(max) - .setFirstResult(start) + .setPage(page) .getResultList(); } } @@ -540,7 +537,7 @@ Whatever the case, the code which orchestrates a unit of work usually just calls public List findBooks(String titlePattern) { var books = sessionFactory.fromTransaction(session -> Queries.findBooksByTitleWithPagination(session, titlePattern, - RESULTS_PER_PAGE, RESULTS_PER_PAGE*page)); + Page.page(RESULTS_PER_PAGE, page)); return books.isEmpty() ? Response.status(404).build() : books; } ---- @@ -550,137 +547,49 @@ That's true, perhaps, but we're much more concerned that it's not very typesafe. Indeed, for many years, the lack of compile-time checking for HQL queries and code which binds arguments to query parameters was our number one source of discomfort with Hibernate. Fortunately, there's now a solution to both problems: as an incubating feature of Hibernate 6.3, we now offer the possibility to have the Metamodel Generator fill in the implementation of such query methods for you. +This facility is the topic of <>, so for now we'll just leave you with one simple example. -[[generated-query-methods]] -=== Generated query methods - -The Metamodel Generator is a standard part of JPA. -// It's an annotation processor that produces a statically-typed metamodel of the entity classes in a Java program. -We've actually already seen its handiwork in the code examples <>: it's the author of the class `Book_` which contains the static metamodel of the <> `Book`. - -[[metamodel-generator]] -.The Metamodel Generator -**** - -:generator: https://hibernate.org/orm/tooling/ -:generator-guide: {userGuideBase}#tooling-modelgen - -Hibernate's {generator}[Metamodel Generator] is an annotation processor that produces what JPA calls a _static metamodel_. -That is, it produces a typed model of the persistent classes in our program, giving us a type-safe way to refer to their attributes in Java code. -In particular, it lets us specify <> and <> in a completely type-safe way. - -Now, you don't have to use the Metamodel Generator with Hibernate—the APIs we just mentioned also accept plain strings—but we find that it works well with Gradle and integrates well with our IDE, and the advantage in typesafety is compelling. - -We've already seen how to set up the annotation processor in the <> we saw earlier. -// You can find more information in the {generator-guide}[User Guide]. -**** - -_Query method generation_ is a new feature of Hibernate's implementation of the Metamodel Generator, an extension to the functionality defined by the JPA specification. -Let's see how it works. - -In an interface or abstract class, write down the "signature" of the query as a function, and specify the HQL or SQL query string itself using a `@HQL` or `@SQL` annotation: - -[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); -} ----- - -The Metamodel Generator verifies that: - -- the parameters of this method match the parameters of the HQL or SQL query, -- the query is syntactically legal and semantically well-typed, that is, that the entities, attributes, and functions referenced in the query actually exist and have compatible types. - -The `@CheckHQL` annotation which instructs Hibernate to validate named queries is not necessary for query methods. - -[TIP] -==== -:query-validator: https://github.com/hibernate/query-validator/ - -Don't like putting queries in annotations? -No problem. -If we set up the {query-validator}[Query Validator], we get validation of queries passed as strings directly to the Hibernate session. -==== - -A query method with a similar signature and return type is generated in the corresponding static metamodel class `Queries_`. -We can call the generated query method like this: - -[source,java] ----- -List books = - Queries_.findBooksByTitleWithPagination(entityManager, titlePattern, - RESULTS_PER_PAGE, page*RESULTS_PER_PAGE); ----- - -Optionally, a query method may have additional "magic" parameters which do not map to query parameters: - -[cols="19,~,32m"] -|=== -| Parameter type | Purpose | Example argument - -| `Page` | Specifies a page of query results | Page.first(20) -| `Order` | Specifies an entity attribute to order by, if `E` is the entity type returned by the query | Order.asc(Book_.title) -| `List` + -(or varargs) | Specifies entity attributes to order by, if `E` is the entity type returned by the query | List.of(Order.asc(Book_.title), Order.asc(Book_.isbn)) -| `Order` | Specifies a column to order by, if the query returns a projection list | Order.asc(1) -| `List` + -(or varargs) | Specifies columns to order by, if the query returns a projection list | List.of(Order.asc(1), Order.desc(2)) -|=== - -Thus, if we redefine our query method as follows: +Suppose we simplify `Queries` to just the following: [source,java] ---- interface Queries { @HQL("from Book where title like :title order by title") - List findBooksByTitleWithPagination(String title, Page page, Order... order); + List findBooksByTitleWithPagination(String title, Page page); } ---- -Then we can call it like this: +Then the Metamodel Generator automatically produces an implementation of the method annotated `@HQL` in a class named `Queries_`. +We can call it just like we called our handwritten version: [source,java] ---- -List books = - Queries_.findBooksByTitleWithPagination(entityManager, titlePattern, - Page.page(RESULTS_PER_PAGE, page), Order.asc(Book_.isbn)); +@GET +@Path("books/{titlePattern}") +public List findBooks(String titlePattern) { + var books = sessionFactory.fromTransaction(session -> + Queries_.findBooksByTitleWithPagination(session, titlePattern, + Page.page(RESULTS_PER_PAGE, page)); + return books.isEmpty() ? Response.status(404).build() : books; +} ---- -A query method doesn't need to return `List`. -It might return a single `Book`. +In this case, the quantity of code eliminated is pretty trivial. +The real value is in improved type safety. +We now find out about errors in assignments of arguments to query parameters at compile time. -[source,java] ----- -@HQL("from Book where isbn = :isbn") -Book findBookByIsbn(String isbn); ----- +[NOTE] +==== +At this point, we're certain you're full of doubts about this idea. +And quite rightly so. +We would love to answer your objections right here, but that will take us much too far off track. +So we ask you to file away these thoughts for now. +We promise to make it make sense when we <>. +And, after that, if you still don't like this approach, please understand that it's completely optional. +Nobody's going to come around to your house to force it down your throat. +==== -It might even return `TypedQuery` or `SelectionQuery`: - -[source,java] ----- -@HQL("from Book where title like :title") -SelectionQuery findBooksByTitle(String title); ----- - -This is extremely useful at times, since it allows the client to further manipulate the query: - -[source,java] ----- -List books = - Queries_.findBooksByTitle(entityManager, titlePattern) - .setOrder(Order.asc(Book_.title)) // order the results - .setPage(Page.page(RESULTS_PER_PAGE, page)) // return the given page of results - .setFlushMode(FlushModeType.COMMIT) // don't flush session before query execution - .setReadOnly(true) // load the entities in read-only mode - .setCacheStoreMode(CacheStoreMode.BYPASS) // don't cache the results - .setComment("Hello world!") // add a comment to the generated SQL - .getResultList(); ----- - -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. +Now that we have a rough picture of what our persistence logic might look like, it's natural to ask how we should test our code. [[testing]] === Testing persistence logic @@ -911,7 +820,8 @@ This introduction will guide you through the basic tasks involved in developing 2. writing a _domain model_, that is, a set of _entity classes_ which represent the persistent types in your program, and which map to tables of your database, 3. customizing these mappings when the model maps to a pre-existing relational schema, 4. using the `Session` or `EntityManager` to perform operations which query the database and return entity instances, or which update the data held in the database, -5. writing complex queries using the Hibernate Query Language (HQL) or native SQL, and, finally -6. tuning performance of the data access logic. +5. using the Hibernate Metamodel Generator to improve compile-time type-safety, +6. writing complex queries using the Hibernate Query Language (HQL) or native SQL, and, finally +7. tuning performance of the data access logic. Naturally, we'll start at the top of this list, with the least-interesting topic: _configuration_.