Hibernate with Panache

Demo Overview

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

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="rest-jackson, jdbc-h2, hibernate-orm-panache, smallrye-openapi"
quarkus extension add rest-jackson jdbc-h2 hibernate-orm-panache smallrye-openapi
✅ Adding extension io.quarkus:quarkus-rest-jackson
✅ 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 Movie Entity

Create a new Movie 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.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;

@Entity
public class Movie extends PanacheEntity {

    public String title;

    @Column(name = "release_date")
    public java.sql.Date releaseDate;
}

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 Movie Resource

Create a new MovieResource 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("/movie")
public class MovieResource {

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

}

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

curl -w '\n' localhost:8080/movie
[]

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 MovieResource 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("/movie")
public class MovieResource {

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

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

}

Now you can insert a new movie by using curl:

curl -w '\n' -d "{\"title\": \"The Empire Strikes Back\", \"releaseDate\": \"1980-05-17\"}" -H "Content-Type: application/json" http://localhost:8080/movie
{"id":1,"title":"The Empire Strikes Back","releaseDate":"1980-05-17"}

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

[
  {
    "id": 1,
    "title": "The Empire Strikes Back",
    "releaseDate": "1980-05-17"
  }
]

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:

INSERT INTO Movie(id,title,release_date) VALUES (1,'A New Hope','1977-05-25');
INSERT INTO Movie(id,title,release_date) VALUES (2,'The Empire Strikes Back','1980-05-17');
INSERT INTO Movie(id,title,release_date) VALUES (3,'Return of the Jedi','1983-05-25');
INSERT INTO Movie(id,title,release_date) VALUES (4,'The Phantom Menace','1999-05-19');
INSERT INTO Movie(id,title,release_date) VALUES (5,'Attack of the Clones','2002-05-16');
INSERT INTO Movie(id,title,release_date) VALUES (6,'Revenge of the Sith','2005-05-19');
ALTER SEQUENCE movie_seq RESTART WITH 7;

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/movie, you should see a response like:

[
  {
    "id": 1,
    "title": "A New Hope",
    "releaseDate": "1977-05-25"
  },
  {
    "id": 2,
    "title": "The Empire Strikes Back",
    "releaseDate": "1980-05-17"
  },
  {
    "id": 3,
    "title": "Return of the Jedi",
    "releaseDate": "1983-05-25"
  },
  {
    "id": 4,
    "title": "The Phantom Menace",
    "releaseDate": "1999-05-19"
  },
  {
    "id": 5,
    "title": "Attack of the Clones",
    "releaseDate": "2002-05-16"
  },
  {
    "id": 6,
    "title": "Revenge of the Sith",
    "releaseDate": "2005-05-19"
  }
]

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 Movie Entity

Update the Movie class to contain a finder method findByYear like:

package com.redhat.developers;

import java.util.List;

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

@Entity
public class Movie extends PanacheEntity {

    public String title;

    @Column(name = "release_date")
    public java.sql.Date releaseDate;

    public static List<Movie> findByYear(int year) {
        return find("YEAR(releaseDate)", year).list();
    }

}

Update the GET REST endpoint to use a QueryParam

Update the MovieResource class by changing the movies 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("/movie")
public class MovieResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Movie> movies(@QueryParam("year") String year) {
        if (year != null) {
            return Movie.findByYear(Integer.parseInt(year));
        }
        return Movie.listAll();
    }

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

Let’s try to filter only the movies from the year 1980:

curl -w '\n' localhost:8080/movie?year=1980
[
  {
    "id": 2,
    "title": "The Empire Strikes Back",
    "releaseDate": "1980-05-17"
  }
]

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 MovieRepository 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 MovieRepository implements PanacheRepository<Movie> {

    public List<Movie> findByYear(int year) {
        return find("YEAR(releaseDate)", year).list();
    }

}

Now you can make another search for movies from a specific year.

Update MovieResource to use MovieRepository

Now let’s update our MovieResource class to use the MovieRepository 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("/movie")
public class MovieResource {

    MovieRepository movieRepository;

    public MovieResource(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Movie> movies(@QueryParam("year") String year) {
        if (year != null) {
            Log.infof("Searching for %s movies", year);
            return movieRepository.findByYear(Integer.parseInt(year));
        }
        return Movie.listAll();
    }

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

}

Let’s try again to filter only the movies with the year 1980:

curl -w '\n' localhost:8080/movie?year=1980
[
  {
    "id": 2,
    "title": "The Empire Strikes Back",
    "releaseDate": "1980-05-17"
  }
]