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.
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 mvn 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:
mvn quarkus:add-extension -Dextension="quarkus-resteasy-jsonb, quarkus-jdbc-h2, quarkus-hibernate-orm-panache, quarkus-smallrye-openapi"
quarkus extension add quarkus-resteasy-jsonb quarkus-jdbc-h2 quarkus-hibernate-orm-panache quarkus-smallrye-openapi
✅ Adding extension io.quarkus:quarkus-resteasy-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 javax.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@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
.
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 javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.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 javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.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:
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Mango','Spring');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Strawberry','Spring');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Orange','Winter');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Lemon','Winter');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Blueberry','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Banana','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Watermelon','Summer');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_sequence'),'Apple','Fall');
INSERT INTO Fruit(id,name,season) VALUES (nextval('hibernate_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 For example: in dev mode, you
can use the configuration |
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 javax.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@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 javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.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 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 javax.enterprise.context.ApplicationScoped;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
@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 javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import io.quarkus.logging.Log;
@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"
}
]