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.
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:
./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.
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 For example: in dev mode, you
can use the configuration |
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"
}
]