A Qute Way to Visualise Data with Panache

In this blog post I’ll show you how develop a Quarkus application to easily access your relational database (using Hibernate ORM with Panache) and display its content in HTML with Qute templates. To break it into more details, in this blog post you will learn:

Download the code
  • What is Hibernate ORM with Panache?
  • How to access a relational database with Hibernate ORM with Panache.
  • What are Qute templates?
  • How to display the data from the database in HTML using Qute templates.
  • Beautify the Qute templates with Tweeter Bootstrap.

Use Case

Let’s say you have a relational database storing a list of books, and your users want a visual interface to query the list of books as well as see the details of a book. There are several tools you can choose from…​ but have you thought of developing it with Quarkus? Quarkus allows you to easily access relational databases, thanks to Hibernate ORM with Panache, as well as using templating thanks to Qute.

The diagram below shows the overall architecture of what will be detailed in this blog post:

  • The Book entity is a Panache entity mapped to a PostgreSQL database (with a Book table).
  • The BookPage is a Qute resource (a JAX-RS endpoint with some Qute specific APIs) that uses two templates (book.html and books.html) to display the list of books and the detail of one book.
  • base.html is a base Qute template, included in the two others, where we’ll add Tweeter Bootstrap.

Accessing the Database with Hibernate ORM with Panache

In Java, we have several APIs and frameworks to map objects to relational databases. One of these famous frameworks is Hibernate which implements the JPA specification. Hibernate makes mapping and querying Java objects easy. Hibernate ORM with Panache is built on top of Hibernate, and makes simple mapping and simple queries trivial.

Panache Entity

To store books into a relational database we use a Book Panache entity. Like a standard JPA entity, Book is annotated with @Entity and can use other JPA annotations (eg. @Table@Column, etc.). One difference though, is that Panache entities extend either PanacheEntity (and therefore get an identifier) or PanacheBaseEntity (and therefore manage their own identifier).

The code below shows the Book Panache entity. As you can see, there is no @Id annotation for the identifier because it inherits it from the super class PanacheEntity. It is annotated with @Entity and uses @Table to specify the name of the table where to map books, as well as @Column to map to specific columns. One difference with straight JPA entities, is that all attributes can be public, and getters and setters become optional.

@Entity
@Table(name = "t_books")
public class Book extends PanacheEntity {

  @Column(length = 100, nullable = false)
  public String title;

  @Column(length = 20)
  public String isbn;

  @Column(nullable = false)
  public BigDecimal price;

  // ....

Active Record Pattern

By inheriting from PanacheBaseEntity, our Book entity benefits from many methods allowing CRUD operations and queries. This is known as the Active Record Pattern. This allows you to do the following:

Book.findById(id);
Book.findByIdOptional(id);
Book.deleteById(id);
Book.deleteAll();
Book.count();

That’s for CRUD operations, but PanacheBaseEntity also has a set of methods to query entities, sort and paginate them:

Book.list(query);
Book.find(query).list();
Book.find(query, Sort.by(sort)).list();
Book.find(query, Sort.by(sort)).page(pageIndex, pageSize).list();

Hibernate ORM with Panache also simplifies the Java Persistence Query Language (JPQL) defined in JPA. It allows you to write queries without the SELECT and FROM clause. You only concentrate on the WHERE clause (without having to use the WHERE keyword):

Book.list("price < 10", Sort.by("isbn"));
Book.list("price < 10 and nbOfPages > 100");
Book.list("price < 10 and nbOfPages > 100", Sort.by("isbn"));
Book.find("price < 10 and nbOfPages > 100", Sort.by("isbn")).list();
Book.find("price < 10 and nbOfPages > 100", Sort.by("isbn")).page(2, 4).list();

Visualising Data with Qute

Now that we have a Book entity with methods to query it, let’s add a few Qute resources to visualise the list of books and the details of a book. Qute is yet another templating engine that fits well with Quarkus. The Quarkus documentation specifies that:

Qute is a templating engine designed specifically to meet the Quarkus needs. The usage of reflection is minimized to reduce the size of native images. The API combines both the imperative and the non-blocking reactive style of coding.

Declaring the Qute Templates

There are different ways to declare Qute templates, but I quite like the type safe way. The idea is to create a Qute resource (here called BookPage) and define the templates with code. In the example below, we define two templates called book() and books().

Notice that these methods can take parameters. The book() method takes a book object so it can access the attributes of a book and display them in HTML. As for books(), it takes a list of books that the template will iterate through and display.

@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {
   @CheckedTemplate
   public static class Templates {
     public static native TemplateInstance book(Book book);
     public static native TemplateInstance books(List<Book> books);
   }
   // ...

The type-safe approach relies on some conventions. The Qute templates must have the same name as defined in the code (eg. book() for book.html). Then, they must be located under the /src/main/resources/templates directory, under a sub-directory named after the Qute resource (here BookPage).

Displaying the Book Details

To display the details of a book, we now need to create a method that accesses the database giving a book identifier. In the BookPage resource, notice the showBookById() method. It uses JAX-RS annotations (@GET@Path@PathParam("id")) so it can handle an HTTP request such as http://localhost:8080/page/books/2. Notice how Book.findById(id) uses the Active Record pattern to get the Book entity by its identifier.

@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {

  @CheckedTemplate
  public static class Templates {
    public static native TemplateInstance book(Book book);
  }

  @GET
  @Path("/{id}")
  public TemplateInstance showBookById(@PathParam("id") Long id) {
    return Templates.book(Book.findById(id));
  }
  // ...

Once the book is found, it is returned withing the book() template. The Qute engine will then look for the template under src/main/resources/templates/BookPage/book.html and pass the book object as a parameter. The book.html file below is quite simple. it uses an expression language to access the attribute of the book object: {book.id} accesses the id attribute of the book object that was passed in Templates.book(Book.findById(id)).

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Book</title>
</head>
<body>
  Id: {book.id}
  Title: {book.title}
  Description: {book.description}
  Price: {book.price}
  Isbn: {book.isbn}
  Number of Pages: {book.nbOfPages}
  Publication Date: {book.publicationDate}
  Created Date: {book.createdDate}
</body>
</html>

Displaying the List of Books

To display the list of books, we need to query them. And that’s when Panache makes life easy. The showAllBooks() method takes the needed parameters to execute a query with sort and pagination. So, for example, if you invoke the showAllBooks() with the following HTTP request:

http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=1&size=5

It will execute the following Panache query:

Book.find("price < 50 and nbOfPages > 100", Sort.by("isbn")).page(1, 5))
@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {

  @CheckedTemplate
  public static class Templates {
    public static native TemplateInstance books(List<Book> books);
  }

  @GET
  public TemplateInstance showAllBooks(
          @QueryParam("query") String query,
          @QueryParam("sort") @DefaultValue("id") String sort,
          @QueryParam("page") @DefaultValue("0") Integer pageIndex,
          @QueryParam("size") @DefaultValue("1000") Integer pageSize) {
    return Templates.books(Book.find(query, Sort.by(sort)).page(pageIndex, pageSize).list())
      .data("query", query)
      .data("sort", sort)
      .data("pageIndex", pageIndex)
      .data("pageSize", pageSize);
  }
  // ...

Notice that when we invoke the books() template, we pass the list of books returned by the query, but we also add the parameters to the template (eg. data("sort", sort)). This is another way to pass data to the template.

The template loops through the list of books ({#for book in books}) and displays the attributes ({book.isbn}{book.id}). To display the data that was passed to the template (data("sort", sort)) we use the special data namespace ({data:sort}).

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Books</title>
</head>
<body>
<code>Book.find({data:query}, Sort.by({data:sort})).page({data:pageIndex}, {data:pageSize}).list()</code>
<table>
  <thead>
  <tr>
    <th scope="col">#</th>
    <th scope="col">Title</th>
    <th scope="col">Isbn</th>
    <th scope="col">Price</th>
    <th scope="col">n° Pages</th>
    <th scope="col">Publication Date</th>
  </tr>
  </thead>
  <tbody>
  {#for book in books}
    <tr>
      <th scope="row"><a href="http://localhost:8080/page/books/{book.id}">{book.id}</a></th>
      <td>{book.title}</td>
      <td>{book.isbn}</td>
      <td>{book.price}</td>
      <td>{book.nbOfPages}</td>
      <td>{book.publicationDate}</td>
    </tr>
  {/for}
  </tbody>
</table>
</body>
</html>

Beautifying the Templates

To beautify these two templates, we can use a base template that both will include. That’s the perfect location to add some Tweeter Bootstrap (CSS and even JavaScript if needed). That’s what the base.html does. It also uses an #insert section used to specify parts that could be overridden by the child templates: here, the title ({#insert title}) and the body ({#insert body}) of the included template:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
  <title>{#insert title}Default Title{/}</title>
</head>
<body>
<div class="container">
  <h1>{#insert title}Default Title{/}</h1>
  {#insert body}No body!{/}
</div>
</body>
</html>

To include the base.html template in the book.html and books.html templates, it is just a matter of including it ({#include base.html}) and overriding the specific sections ({#title}{/title} and {#body}{/body}).

{#include base.html}
{#title}{books.size} Books{/title}
{#body}
  <!-- body -->
{/body}
{/include}

Executing the Application

To execute the code, you need Docker to be up and running. Why? Because the data is stored into a PostgreSQL database and we use Quarkus DevServices to automatically start it. The way DevServices works, is that it detects that the application needs a PostgreSQL database (because it is declared as a dependency in the pom.xml) and uses TestContainers behind the scenes, to download the Docker image, start and stop it.

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

This happens just by starting Quarkus with the following command:

$ mvn quarkus:dev

Then, you can point your browser to the following URLs so you can query the database and display different lists of books:

http://localhost:8080/page/books?query=price < 10
http://localhost:8080/page/books?query=price < 10 and nbOfPages > 100 &sort=isbn
http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=1&size=5
http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=2&size=5

To get the details of a book, you can use the following URLs:

http://localhost:8080/page/book/1
http://localhost:8080/page/book/2

Conclusion

There are several ways to easily display data from the database. In this blog post I wanted to show you the combination of Qute and Hibernate ORM with PanacheQute is a templating engine, so its main purpose is not to be yet another front-end framework. You can use Qute templates to write emails, send messages, etc. But writing HTML pages is also doable.

Download the code

As for Hibernate ORM with Panache, it is a great way to make easy queries trivial. Remember that Hibernate ORM with Panache is built on top of JPA. So if you need the power of JPA, you can. By inheriting from PanacheEntity you get access to the JPA EntityManager and can use it whenever you want. You are not stuck to Hibernate ORM with Panache, you also have the full powser of JPA.

Now it’s your turn. Download the code and give it a try.

References

If you want to give this code a try, download it from GitHub, build it, run it, and make sure to break the communication between the microservices to see fallback in action.

You can get my books and on-line courses on Quarkus.

One thought on “A Qute Way to Visualise Data with Panache

Leave a Reply