Hibernate with Panache

Demo Overview

You’ll learn how easy and productive is Quarkus with Hibernate with Panache. For this, we’ll develop a simple CRUD REST API that handles information about fruits.

We’ll use H2 as our backing database in this section, but it’s very easy to use other database engines with Quarkus as you will see in the Dev Services chapter. As an exercise for later, we suggest to try your favorite database engine with the documentation found here.

Adding Extensions

Quarkus provides a lot of optimized dependencies to its ecosystem through extensions. For this particular chapter, we’ll need to add extensions that enables us to work with H2, Hibernate ORM, Panache (a novel persistence API), and JSON.

You probably still have ./mvnw quarkus:dev running in your terminal. And that’s perfectly fine!

Just open a new terminal window, and make sure you’re at the root of your tutorial-app project, then run:

  • Maven

  • Quarkus CLI

./mvnw quarkus:add-extension -Dextension="quarkus-resteasy-reactive-jsonb, quarkus-jdbc-h2, quarkus-hibernate-orm-panache, quarkus-smallrye-openapi"
quarkus extension add quarkus-resteasy-reactive-jsonb quarkus-jdbc-h2 quarkus-hibernate-orm-panache quarkus-smallrye-openapi
✅ Adding extension io.quarkus:quarkus-resteasy-reactive-jsonb
✅ Adding extension io.quarkus:quarkus-smallrye-openapi
✅ Adding extension io.quarkus:quarkus-hibernate-orm-panache
✅ Adding extension io.quarkus:quarkus-jdbc-h2
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.969 s
[INFO] Finished at: 2020-05-11T21:32:14-04:00
[INFO] ------------------------------------------------------------------------

You’ll notice that by running this command the Quarkus maven plugin added some dependencies to your pom.xml file. And best of all: Quarkus will autodetect and apply the changes, and you don’t even need to restart Quarkus!

Adding database properties to your configuration

Add the following database properties to your application.properties so that it looks like:

# Configuration file
# key = value
greeting=Hello y'all!
quarkus.datasource.jdbc.url=jdbc:h2:mem:default
quarkus.datasource.db-kind=h2
quarkus.hibernate-orm.database.generation=drop-and-create
With [Dev Services] enabled, no JDBC URL needs to be provided in Dev Mode. In this case, we input the URL to ensure consistency across all application run modes.

Create Fruit Entity

Create a new Fruit Java class in src/main/java in the com.redhat.developers package with the following contents:

package com.redhat.developers;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;

@Entity
public class Fruit extends PanacheEntity {

    public String name;

    public String season;

}

Notice that we’re not providing an @Id, nor we’re creating the getters and setters. Don’t worry. It’s a Panache feature. By extending PanacheEntity, we’re using the Active Record persistence pattern instead of a DAO. This means that all persistence methods are blended with our own Entity.

What is 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 it does not make simple and common mappings trivial. Hibernate ORM with Panache focuses on making your entities trivial and fun to write in Quarkus.

Fore more information please refer to the Panache Guide

Create Fruit Resource

Create a new FruitResource Java class in src/main/java in the com.redhat.developers package with the following contents:

package com.redhat.developers;

import java.util.List;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/fruit")
public class FruitResource {

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

}

Now we should everything in place to query our GET REST endpoint:

curl localhost:8080/fruit
[]

We have an empty JSON array as the response, which is expected, since our database is currently empty.

Adding a POST endpoint

Let’s change our FruitResource class to also contain a POST REST endpoint:

package com.redhat.developers;

import java.util.List;

import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;

@Path("/fruit")
public class FruitResource {

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

    @Transactional
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response newFruit(Fruit fruit) {
        fruit.id = null;
        fruit.persist();
        return Response.status(Status.CREATED).entity(fruit).build();
    }

}

Now you can insert a new fruit by using curl:

curl -d "{\"name\": \"Banana\", \"season\": \"Summer\"}" -H "Content-Type: application/json" http://localhost:8080/fruit
{"id":1,"name":"Banana","season":"Summer"}

Now if you refresh your browser pointing to http://localhost:8080/fruit, you should see a response like:

[
  {
    "id": 1,
    "name": "Banana",
    "season": "Summer"
  }
]

Creating custom finders

We’re using H2, which is an in-memory database. This means that every time Quarkus restarts, we’ll lose all the information we have provided.

To provide some meaningful results for our custom finder, let’s create some initial data to be populated to our database.

Create the file import.sql in the folder src/main/resources with the following content:

create sequence fruit_sequence start with 1 increment by 1;
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Mango','Spring');
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Strawberry','Spring');
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Orange','Winter');
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Lemon','Winter');
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Blueberry','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Banana','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Watermelon','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Apple','Fall');
INSERT INTO Fruit(id,name,season) VALUES (nextval('fruit_sequence'),'Pear','Fall');

And append the following configuration in application.properties:

quarkus.hibernate-orm.sql-load-script=import.sql

Now if you refresh your browser pointing to http://localhost:8080/fruit, you should see a response like:

[
  {
    "id": 1,
    "name": "Mango",
    "season": "Spring"
  },
  {
    "id": 2,
    "name": "Strawberry",
    "season": "Spring"
  },
  {
    "id": 3,
    "name": "Orange",
    "season": "Winter"
  },
  {
    "id": 4,
    "name": "GrapeFruit",
    "season": "Winter"
  },
  {
    "id": 5,
    "name": "Blueberry",
    "season": "Summer"
  },
  {
    "id": 6,
    "name": "Banana",
    "season": "Summer"
  },
  {
    "id": 7,
    "name": "Plum",
    "season": "Summer"
  },
  {
    "id": 8,
    "name": "Apple",
    "season": "Fall"
  },
  {
    "id": 9,
    "name": "Grapes",
    "season": "Fall"
  }
]

You can add different import.sql files based on the application profile.

For example: in dev mode, you can use the configuration quarkus.hibernate-orm.sql-load-script=import-dev.sql, while in production mode you can use quarkus.hibernate-orm.sql-load-script=import-prod.sql.

Adding a custom finder to the Fruit Entity

Update the Fruit class to contain a finder method findBySeason like:

package com.redhat.developers;

import java.util.List;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;

@Entity
public class Fruit extends PanacheEntity {

    public String name;

    public String season;

    public static List<Fruit> findBySeason(String season) {
        return find("season", season).list();
    }

}

Update the GET REST endpoint to use a QueryParam

Update the FruitResource class by changing the fruits method to use a @QueryParam:

package com.redhat.developers;

import java.util.List;

import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;

@Path("/fruit")
public class FruitResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Fruit> fruits(@QueryParam("season") String season) {
        if (season != null) {
            return Fruit.findBySeason(season);
        }
        return Fruit.listAll();
    }

    @Transactional
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response newFruit(Fruit fruit) {
        fruit.id = null;
        fruit.persist();
        return Response.status(Status.CREATED).entity(fruit).build();
    }

}

Let’s try to filter only the fruits with the Summer season:

curl localhost:8080/fruit?season=Summer
[
  {
    "id": 5,
    "name": "Blueberry",
    "season": "Summer"
  },
  {
    "id": 6,
    "name": "Banana",
    "season": "Summer"
  },
  {
    "id": 7,
    "name": "Watermelon",
    "season": "Summer"
  }
]

Using Repository instead of ActiveRecord pattern

Is PanacheEntity too opinionated for you? Maybe you prefer the traditional Repository pattern? Don’t worry: we’ve got you covered.

Panache also helps you to create Repositories.

Create the FruitRepository Java class in src/main/java in the com.redhat.developers package with the following contents:

package com.redhat.developers;

import java.util.List;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class FruitRepository implements PanacheRepository<Fruit> {

    public List<Fruit> findBySeason(String season) {
        return find("upper(season)", season.toUpperCase()).list();
    }

}

Now you can make an case-insensitive search for fruits belonging to a specific season.

Update FruitResource to use FruitRepository

Now let’s update our FruitResource class to use the FruitRepository we just created:

package com.redhat.developers;

import java.util.List;

import io.quarkus.logging.Log;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;

@Path("/fruit")
public class FruitResource {

    FruitRepository fruitRepository;

    public FruitResource(FruitRepository fruitRepository) {
        this.fruitRepository = fruitRepository;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Fruit> fruits(@QueryParam("season") String season) {
        if (season != null) {
            Log.infof("Searching for %s fruits", season);
            return fruitRepository.findBySeason(season);
        }
        return Fruit.listAll();
    }

    @Transactional
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response create(Fruit fruit) {
        fruit.id = null;
        fruit.persist();
        return Response.status(Status.CREATED).entity(fruit).build();
    }

}

Let’s try again to filter only the fruits with the Spring season:

curl localhost:8080/fruit?season=Spring
[
  {
    "id": 1,
    "name": "Mango",
    "season": "Spring"
  },
  {
    "id": 2,
    "name": "Strawberry",
    "season": "Spring"
  }
]