Hibernate ORM with Panache

Hibernate ORM is the de facto JPA implementation and offers you the full breadth of an Object Relational Mapper. It makes complex mappings possible, but many simple and common mappings can also be complex. Hibernate ORM with Panache focuses on making your entities trivial and fun to write and use with Quarkus.

With Panache, we took an opinionated approach to make hibernate as easy as possible. Hibernate ORM with Panache offers the following:

  • Auto-generation of IDs (but you can still customize if needed)

  • No need for getters/setters. You simply refer to fields directly like person.name instead of person.getName(). You can still create getters and setters if you need to!

  • Lots of super-useful static methods for access like listAll(), find(), etc.

  • No need for custom queries for basic operations, e.g. Person.find("name", "stef")

That’s all there is to it: with Panache, Hibernate ORM has never looked so trim and neat.

People

You may have wondered why we are using people in our Java APIs, and now you’ll find out; we’ll be creating a simple data-driven application that manages a set of people, who have a name, birth date, and eye color. In the previous lab we added the extension for Panache to our project, but we will need a few more for connecting to databases. Add these extensions with the following command:

mvn quarkus:add-extension -Dextensions="hibernate-orm-panache, jdbc-postgresql"

You should see:

[INFO] [SUCCESS] ✅  Extension io.quarkus:quarkus-hibernate-orm-panache has been installed
[INFO] [SUCCESS] ✅  Extension io.quarkus:quarkus-jdbc-postgresql has been installed

We’ll use the Postgres in both your local and the production Kubernetes environment. The resteasy-reactive-jackson is used to encode JSON objects.

Quarkus Dev Services

When testing or running in dev mode Quarkus can provide you with a zero-config database out of the box, a feature we refer to as Dev Services. Depending on your database type you may need Docker or Podman installed in order to use this feature. Dev Services is supported for the PostgreSQL databases. With that, you don’t need to add required configurations (e.g., username, password, JDBC URL) to set up the PostgreSQL in the application.properties file.

Red Hat Dev Spaces enables you to run the Quarkus Dev Services in the terminal using the KUBEDOCK tool. Unlikely a local environment, you need to set the quarkus.datasource.devservices.volumes where the PostgreSQL’s data is stored inside a container.

when you take a look at the src/main/resources/application.properties, you can see the following configuration which we already added for you in advance.

%dev.quarkus.datasource.devservices.volumes."/"=/var/lib/postgresql/

Find more information about the Quarkus Dev Services here.

When you take a look at the 02. Start Live Coding terminal, you should see the following logs:

Dev Services for the default datasource (postgresql) started

Then, execute podman ps in the bash terminal. You will see that a PostgreSQL container is running automatically.

CONTAINER ID  IMAGE                  COMMAND     CREATED         STATUS        PORTS       NAMES
27ee26a42fae  docker.io/postgres:14              24 seconds ago  Healthy (Up)              27ee26a42faebacacfcccacde227e210b6884f66ffd7dcf13f4470c56ba7a8e7

The database connection details (JDBC url, database credentials) are specified in application.properties using the Quarkus Profile support. So for example you’ll find this:

%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://postgres-database/person
%prod.quarkus.datasource.jdbc.driver=org.postgresql.Driver
...

Create Entity

With our extension installed, we can now define our entity using Panache.

We’ll first need the definition of eye colors, so let’s create an enum. Create a new java class file in src/main/java/org/acme/people/model called EyeColor.java, and replace the following enum definition:

package org.acme.people.model;

public enum EyeColor {
    BLUE, GREEN, HAZEL, BROWN
}

This app will be a database of people, each of which have a name, birthdate, and eye color. We’ll need an entity, so open up the src/main/java/org/acme/people/model/Person.java file, and replace the stub code in the file with the following:

package org.acme.people.model;

import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

import org.acme.people.model.EyeColor;

@Entity
public class Person extends PanacheEntity {
    // the person's name
    public String name;

    // the person's birthdate
    public LocalDate birth;

    // the person's eye color
    @Enumerated(EnumType.STRING)
    @Column(length = 8)
    public EyeColor eyes;

    // TODO: Add more queries
}

You will incrementally replace the // TODO elements as we go along, so don’t delete them just yet!

Extending PanacheEntity is referred to as the Active Record Pattern. There is also another pattern called the Repository Pattern that you could use which involves separating your entities from the operations on those entities.

As you can see we’ve defined the three fields name, birth, and eyes. We’re using the Java Persistence API’s @Enumerated field type for our eye color.

Define RESTful endpoint

Next, we’ll create a PersonResource class which we will use for our RESTful endpoint. Create a new Java class in the src/main/java/org/acme/people/rest directory called PersonResource.java with the following code:

package org.acme.people.rest;

import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

import org.acme.people.model.DataTable;
import org.acme.people.model.EyeColor;
import org.acme.people.model.Person;
import org.acme.people.utils.CuteNameGenerator;

import io.quarkus.panache.common.Parameters;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.hibernate.orm.panache.PanacheQuery;

@Path("/person")
@ApplicationScoped
public class PersonResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Person> getAll() {
        return Person.listAll();
    }

    // TODO: add basic queries

    // TODO: add datatable query

    // TODO: Add lifecycle hook

}

You may see lots of warnings about unused imports. Ignore them, we’ll use them later!

As you can see we’ve implemented our first Panache-based query, the getAll method, which will return our list of people as a JSON object when we access the GET /person endpoint. This is defined using standard JAX-RS @Path and @GET and @Produces annotations.

Add sample data

Let’s add some sample data to the database so we can test things out. Create a new file src/main/resources/import.sql and add some SQL statements to the file to run on startup:

Add these lines to import.sql file you just created:

INSERT INTO person(id, name, birth, eyes) VALUES (1, 'Farid Ulyanov', '1974-08-15', 'BLUE');
INSERT INTO person(id, name, birth, eyes) VALUES (2, 'Salvador L. Witcher', '1984-05-24', 'BROWN');
INSERT INTO person(id, name, birth, eyes) VALUES (3, 'Kim Hu', '1999-04-25', 'HAZEL');
ALTER SEQUENCE person_seq RESTART WITH 4;

These statements will add some fake people to our database on startup.

Test the app

With the app running, let’s try out our first RESTful endpoint to retrieve all the sample users. Open up a separate Terminal and issue the following command:

curl -s http://localhost:8080/person | jq

We call the endpoint with curl then send the output through jq to make the output prettier. You should see:

[
  {
    "id": 1,
    "birth": "1974-08-15",
    "eyes": "BLUE",
    "name": "Farid Ulyanov"
  },
  {
    "id": 2,
    "birth": "1984-05-24",
    "eyes": "BROWN",
    "name": "Salvador L. Witcher"
  },
  {
    "id": 3,
    "birth": "1999-04-25",
    "eyes": "HAZEL",
    "name": "Kim Hu"
  }
]

It’s working! Note that the id field was added to our entity, but never appear in our query APIs and can be safely ignored most of the time.

Advanced use cases may require a custom ID strategy, which can by done by extending PanacheEntityBase instead of PanacheEntity, and declaring a public id field with the necessary policy. For example (do not copy this code into your app):

@Id
@SequenceGenerator(
          name = "personSequence",
          sequenceName = "person_id_seq",
          allocationSize = 1,
          initialValue = 4)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSequence")
public Integer id;

Add Basic Queries

Let’s modify the application and add some queries. Much like traditional object-oriented programming, Panache and Quarkus recommend you place your custom entity queries as close to the entity definition as possible, in this case in the entity definition itself. Open the Person entity class (it’s in the org.acme.person.model package), and add the following code under the // TODO: Add more queries comment:

    public static List<Person> findByColor(EyeColor color) {
        return list("eyes", color);
    }

    public static List<Person> getBeforeYear(int year) {
        return Person.<Person>streamAll()
        .filter(p -> p.birth.getYear() <= year)
        .collect(Collectors.toList());
    }

These two queries will find a list of people in our database based on eye color, or birth year. Note the getBeforeYear is implemented using the Java Streams API.

All list methods in Panache-based entities (those that extend from PanacheEntity) have equivalent stream versions. So list has a stream variant, listAll-→`streamAll` and so on.

With our custom entity queries implemented in our Person entity class, let’s add RESTful endpoints to PersonResource to access them.

Open the PersonResource class and add two news endpoint under the //TODO: add basic queries comment:

    @GET
    @Path("/eyes/{color}")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Person> findByColor(@PathParam(value = "color") EyeColor color) {
        return Person.findByColor(color);
    }

    @GET
    @Path("/birth/before/{year}")
    @Produces(MediaType.APPLICATION_JSON)
    public List<Person> getBeforeYear(@PathParam(value = "year") int year) {
        return Person.getBeforeYear(year);
    }

Inspect the results

Check that it works as expected by testing the new endpoints. Let’s find all the people with BLUE eyes. Execute in your Terminal:

curl -s http://localhost:8080/person/eyes/BLUE | jq

You should only see one person with BLUE eyes:

[
  {
    "id": 1,
    "birth": "1974-08-15",
    "eyes": "BLUE",
    "name": "Farid Ulyanov"
  }
]

And let’s find people born in 1990 or earlier:

curl -s http://localhost:8080/person/birth/before/1990 | jq

You should see two people born in 1990 or earlier:

[
  {
    "id": 1,
    "name": "Farid Ulyanov",
    "birth": "1974-08-15",
    "eyes": "BLUE"
  },
  {
    "id": 2,
    "name": "Salvador L. Witcher",
    "birth": "1984-05-24",
    "eyes": "BROWN"
  }
]

The Person entity’s superclass comes with lots of super useful static methods and you can add your own in your entity class. Users can just start using your entity Person by typing Person, and getting completion for all the operations in a single place.

Add Paging and Filtering

In the previous step you added a few more custom queries to your entity and the associated RESTful endpoints. In this step we’ll build a slightly more complex query including filtering, searching and paging capabilities.

Showing data in tables

Earlier we used curl to access our data, which is very useful for testing, but for real applications you will usually surface the data in other ways, like on web pages using tables, with options for searching, sorting, filtering, paging, etc. Quarkus and Panache make this easy to adapt your application for any display library or framework.

Let’s use a popular jQuery-based plugin called DataTables. It features a server-side processing mode where it depends on the server (in this case our Quarkus app) to do searching, filtering, sorting, and paging. This is useful for very large datasets, on the order of hundreds of thousands of records or more. Transmitting the entire data set to the client browser is inefficient at best, and will crash browsers, increase networking usage, and frustrate users at worst. So let’s just return the exact data needed to be shown.

Add Datatables endpoint

DataTables documentation shows that its frontend will call an endpoint on the backend to retrieve some amount of data. It will pass several query parameters to tell the server what to sort, filter, search, and which data to return based on the page size and current page the user is viewing. For this example, we’ll only support a subset:

  • start - The index of the first element needed

  • length - Total number records to return (or less, if there are less records that meet criteria)

  • search[value] - The value of the search box

  • draw - DataTables does asnychronous processing, so this value is sent with each request, expecting it to be returned as-is, so DataTables can piece things back together on the frontend if a user selects things quickly.

Open the PersonResource resource class and add the following code below the // TODO: add datatable query comment:

    @GET
    @Path("/datatable")
    @Produces(MediaType.APPLICATION_JSON)
    public DataTable datatable(
        @QueryParam(value = "draw") int draw,
        @QueryParam(value = "start") int start,
        @QueryParam(value = "length") int length,
        @QueryParam(value = "search[value]") String searchVal

        ) {
            // TODO: Begin result

            // TODO: Filter based on search

            // TODO: Page and return

    }

You will see syntax errors highlighted in the editor when you paste the code. We’ll fix those in the next step!

Here we are using JAX-RS @QueryParam values to specify the incoming parameters and be able to use them when the frontend calls the GET /person/datatable endpoint.

We’ll fill in the TODO comments to build this method.

Implement /datatable endpoint

DataTables requires a specific JSON payload to be returned from this, and we’ve pre-created a POJO DataTable class representing this structure in src/main/java/org/acme/people/model/DataTable.java. This simple structure includes these fields:

  • draw - The async processing record id

  • recordsTotal - Total records in database

  • recordsFiltered - Total records that match filtering criteria

  • data - The actual array of records

  • error - Error string, if any

So, in our PersonResource endpoint, we’ll start with an empty result object using the pre-created DataTable model. Add this code below the // TODO: Begin Result comment:

            DataTable result = new DataTable();
            result.setDraw(draw); (1)
1 We initialize the DataTable return object with the value passed in, to ensure DataTables redraws in the correct order in case of async returns.

Implement search logic

Next, if the request includes a search parameter, let’s take care of that by including a search query, otherwise just collect all records. Add this code below the // TODO: Filter based on search marker:

            PanacheQuery<Person> filteredPeople;

            if (searchVal != null && !searchVal.isEmpty()) { (1)
                filteredPeople = Person.<Person>find("name like :search",
                    Parameters.with("search", "%" + searchVal + "%"));
            } else {
                filteredPeople = Person.findAll();
            }
1 If a search value was passed in, use it to search using the Panache find method. Otherwise, use findAll to skip filtering.

Implement paging logic

And finally, we use the built-in Panache page operator to seek to the correct page of records and stream the number of entries desired, set the values into the result and return it. Add this code below the // TODO: Page and return marker:

            int page_number = start / length;
            filteredPeople.page(page_number, length);

            result.setRecordsFiltered(filteredPeople.count());
            result.setData(filteredPeople.list());
            result.setRecordsTotal(Person.count());

            return result;

Test the result

Let’s test out our new endpoint using curl to search for names with yan in their name. Execute this in the Terminal:

curl -s "http://localhost:8080/person/datatable?draw=1&start=0&length=10&search\[value\]=yan" | jq

This should return a single entity (since in our 3-person sample data, only one has yan in their name), embedded in the return object that DataTable is expecting (with the draw, recordsFiltered, recordsTotal etc):

{
  "draw": 1,
  "recordsTotal": 3,
  "recordsFiltered": 1,
  "data": [
    {
      "id": 1,
      "name": "Farid Ulyanov",
      "birth": "1974-08-15",
      "eyes": "BLUE"
    }
  ],
  "error": null
}

The data, draw, recordsFiltered and recordsTotal values are what the DataTables frontend will be expecting when it calls this endpoint.

Add lifecycle hook

You often need to execute custom actions when the application starts and clean up everything when the application stops. In this case we’ll add an action that will pre-generate a lot of fake data.

Managed beans (like our PersonResource) can listen for lifecycle events by using the @Observes annotation on method signatures, which will be called when the associated event occurs.

Open the PersonResource resource class and add the following lifecycle listener at the // TODO: Add lifecycle hook marker:

    @Transactional
    void onStart(@Observes StartupEvent ev) {
        for (int i = 0; i < 1000; i++) {
            String name = CuteNameGenerator.generate();
            LocalDate birth = LocalDate.now().plusWeeks(Math.round(Math.floor(Math.random() * 40 * 52 * -1)));
            EyeColor color = EyeColor.values()[(int)(Math.floor(Math.random() * EyeColor.values().length))];
            Person p = new Person();
            p.birth = birth;
            p.eyes = color;
            p.name = name;
            Person.persist(p);
        }
    }

This code will insert 1,000 fake people with random birthdates in the last 40 years, eye colors, and names at startup. Note the use of the @Transactional annotation - this is required for methods that make changes to the underlying database (and automatically executes the method in a Transaction for you).

Access new data

Although our lifecycle code is listening for StartupEvent, and our application has already started, in quarkus:dev mode Quarkus will still fire this event once. So let’s test it out and see if it picks up our new data. We’ll search for a single letter F and limit the results to 2:

curl -s "http://localhost:8080/person/datatable?draw=1&start=0&length=2&search\[value\]=F" | jq

Adding 1k entries will make startup time artificially high, around 1 second.

You should get up to 2 records returned (since we passed length=2 in the query), but the total number available should show many more indicating our search found many more, and the total number of records should now be 1003 (the 1k we added plus the 3 original values):

{
  "draw": 1,
  "recordsTotal": 1003,
  "recordsFiltered": 140, (1)
  "data": [
    {
      "id": 1,
      "name": "Farid Ulyanov",
      "birth": "1974-08-15",
      "eyes": "BLUE"
    },
    {
      "id": 9,
      "name": "Crimson Ferret",
      "birth": "1988-03-10",
      "eyes": "BROWN"
    }
  ],
  "error": null
}
1 Note the values for recordsFiltered (the number of records with the letter F in the name), and recordsTotal. The value you see for recordsFiltered may be different than the above value, since the number of records with an F in the name may vary since the data is random. But the recordsTotal shows our initial 3 values, plus the 1000 additional values we added in the lifecycle hook code.

Deploy to OpenShift

Our production environment needs a "real" database so let’s deploy a Postgres database to OpenShift.

Back in the Developer Sandbox and the OpenShift console in the Developer Perspective click the Topology View

Select +Add to Project on the left. Then, Type in PostgreSQL (Ephemeral) in the search box, and select the PostgreSQL (ephemeral):

db

Select Instantiate Template and fill in the following fields, leaving the others as their default values:

  • Namespace (the first one): Select %USER_ID%-dev

  • Namespace (the second one): openshift

  • Database Service Name: postgres-database

  • PostgreSQL Connection Username: sa

  • PostgreSQL Connection Password: sa

  • PostgreSQL Database Name: person

db

Select Create.

And label the items with proper icons by running these commands in your terminal in DevSpaces:

oc label dc/postgres-database  app.openshift.io/runtime=postgresql --overwrite && \
oc label deployment/people app.kubernetes.io/part-of=people --overwrite && \
oc label dc/postgres-database app.kubernetes.io/part-of=people --overwrite && \
oc annotate deployment/people app.openshift.io/connects-to=postgres-database --overwrite

This will deploy the database to our new project. View the changes in the Topology view in the Developer Perspective:

inventory_db_deployments

Rebuild and redeploy app

In previous steps we deployed our sample application as a native binary. Now let’s switch to a JVM-based deployment from here on out.

Now let’s deploy the application itself. Run the following command which will build and deploy using the OpenShift extension:

oc delete bc/people && \
mvn clean package -DskipTests

The output should end with BUILD SUCCESS.

Finally, make sure it’s actually done rolling out.

oc rollout status -w deployment/people

Wait for that command to report deployment "people" successfully rolled out before continuing.

And let’s do again to label the items with proper icons:

oc label deployment/people app.kubernetes.io/part-of=people --overwrite && \
oc label dc/postgres-database app.kubernetes.io/part-of=people --overwrite && \
oc annotate deployment/people app.openshift.io/connects-to=postgres-database --overwrite

Back on the Topology View, make sure it’s done deploying (dark blue circle):

Remember in our application.properties we have pre-defined database connections for the %prod profile. When Quarkus runs in production, it will connect to the database you just deployed!

inventory_db_deployments

Access deployed app

Now that we have our app running on OpenShift, let’s see what we can do.

We can access using curl once again to find everyone born in or before the year 2000 (there will be many).

curl -s https://$(oc get route people -o=go-template --template='{{ .spec.host }}')/person/birth/before/2000 | jq

Now lets access our GUI for this application. Run the below command to get the URL for your application

oc get route people -o=go-template --template='http://{{ .spec.host }}/datatable.html {{printf "\n"}}'

Open a new browser tab and paste the result to view the page

The url should look something like this:

http://people-user-dev.apps.sandbox-m2.ll9k.p1.openshiftapps.com/datatable.html

The page should look like this:

datatable

Notice the total number of records reported at the bottom. Type in a single letter, e.g. F in the search box and see how responsive the app is. Type additional letters to narrow the search. Rather than having all records loaded in the browser, DataTable makes a call back to our /person/datatable REST endpoint to fetch only those records that should be shown, based on page size, current page you’re looking at, and any search filters. With a page size of 10 each REST call will only return up to 10 records, no matter how many there are in the database.

Skip around a few pages, try some different searches, and notice that the data is only loaded when needed. The overall performance is very good even for low-bandwidth connections or huge data sets.

Congratulations

In this exercise you got a glimpse of the power of Quarkus apps when dealing with large amounts of data. There is much more to Quarkus than fast startup times and low resource usage, so keep going!