An Advanced GraphQL with Quarkus
In this article, you will learn how to create a GraphQL application using the Quarkus framework. Our application will connect to a database, and we will use the Quarkus Panache module as the ORM provider. On the other hand, Quarkus GraphQL support is built on top of the SmallRye GraphQL library. We will discuss some more advanced GraphQL and JPA topics like dynamic filtering or relations fetching.
As an example, I will use the same application as in my previous article about Spring Boot GraphQL support. We will migrate it to Quarkus. Instead of the Netflix DGS library, we will use the already mentioned SmallRye GraphQL module. The next important challenge is to replace the ORM layer based on Spring Data with Quarkus Panache. If you would like to know more about GraphQL on Spring Boot read my article An Advanced GraphQL with Spring Boot and Netflix DGS.
Source Code
If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. After that go to the sample-app-graphql
directory. Then you should just follow my instructions.
We use the same schema and entity model as in my previous article about Spring Boot and GraphQL. Our application exposes GraphQL API and connects to H2 in-memory database. There are three entities Employee
, Department
and Organization
– each of them stored in the separated table. Let’s take a look at a visualization of relations between them.
1. Dependencies for Quarkus GraphQL
Let’s start with dependencies. We need to include SmallRye GraphQL, Quarkus Panache, and the io.quarkus:quarkus-jdbc-h2
artifact for running an in-memory database with our application. In order to generate getters and setters, we can include the Lombok library. However, we can also take an advantage of the Quarkus auto-generation support. After extending entity class with PanacheEntityBase
Quarkus will also generate getters and setters. We may even extend PanacheEntity
to use the default id.
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-graphql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
2. Domain Model for GraphQL and Hibernate
In short, Quarkus simplifies the creation of GraphQL APIs. We don’t have to manually define any schemas. The only thing we need to do is create a domain model and use some annotations. First things first – our domain model. To clarify, I’m using the same classes for ORM and API. Of course, we should create DTO objects to expose data as a GraphQL API, but I want to simplify our example implementation as much as I can. Here’s the Employee
entity class.
@Entity
@Data
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Employee {
@Id
@GeneratedValue
@EqualsAndHashCode.Include
private Integer id;
private String firstName;
private String lastName;
private String position;
private int salary;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
@ManyToOne(fetch = FetchType.LAZY)
private Organization organization;
}
Also, let’s take a look at the Department
entity.
@Entity
@Data
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Department {
@Id
@GeneratedValue
@EqualsAndHashCode.Include
private Integer id;
private String name;
@OneToMany(mappedBy = "department")
private Set<Employee> employees;
@ManyToOne(fetch = FetchType.LAZY)
private Organization organization;
}
Besides entities, we also have input parameters used in the mutations. However, the input objects are much simpler than outputs. Just to compare, here’s the DepartmentInput
class.
@Data
@NoArgsConstructor
public class DepartmentInput {
private String name;
private Integer organizationId;
}
3. GraphQL Filtering with Quarkus
In this section, we will create a dynamic filter in GraphQL API. Our sample filter allows defining criteria for three different Employee
fields: salary
, age
and position
. We may set a single field, two of them or all. Each condition is used with the AND
relation to other conditions. The class with a filter implementation is visible below. It consists of several fields represented by the FilterField
objects.
@Data
public class EmployeeFilter {
private FilterField salary;
private FilterField age;
private FilterField position;
}
Then, let’s take a look at the FilterField
implementation. It has two parameters: operator
and value
. Basing on the values of these parameters we are generating a JPA Criteria
Predicate
. I’m just generating the most common conditions for comparison between two numbers or strings.
@Data
public class FilterField {
private String operator;
private String value;
public Predicate generateCriteria(CriteriaBuilder builder, Path field) {
try {
int v = Integer.parseInt(value);
switch (operator) {
case "lt": return builder.lt(field, v);
case "le": return builder.le(field, v);
case "gt": return builder.gt(field, v);
case "ge": return builder.ge(field, v);
case "eq": return builder.equal(field, v);
}
} catch (NumberFormatException e) {
switch (operator) {
case "endsWith": return builder.like(field, "%" + value);
case "startsWith": return builder.like(field, value + "%");
case "contains": return builder.like(field, "%" + value + "%");
case "eq": return builder.equal(field, value);
}
}
return null;
}
}
After defining model classes we may proceed to the repository implementation. We will use PanacheRepository
for that. The idea behind that is quite similar to the Spring Data Repositories. However, we don’t have anything similar to the Spring Data Specification
interface which can be used to execute JPA criteria queries. Since we need to build a query basing on dynamic criteria, it would be helpful. Assuming that, we need to inject EntityManager
into the repository class and use it directly to obtain JPA CriteriaBuilder
. Finally, we are executing a query with criteria and returning a list of employees matching input conditions.
@ApplicationScoped
public class EmployeeRepository implements PanacheRepository<Employee> {
private EntityManager em;
public EmployeeRepository(EntityManager em) {
this.em = em;
}
public List<Employee> findByCriteria(EmployeeFilter filter) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Employee> criteriaQuery = builder.createQuery(Employee.class);
Root<Employee> root = criteriaQuery.from(Employee.class);
Predicate predicate = null;
if (filter.getSalary() != null)
predicate = filter.getSalary().generateCriteria(builder, root.get("salary"));
if (filter.getAge() != null)
predicate = (predicate == null ?
filter.getAge().generateCriteria(builder, root.get("age")) :
builder.and(predicate, filter.getAge().generateCriteria(builder, root.get("age"))));
if (filter.getPosition() != null)
predicate = (predicate == null ? filter.getPosition().generateCriteria(builder, root.get("position")) :
builder.and(predicate, filter.getPosition().generateCriteria(builder, root.get("position"))));
if (predicate != null)
criteriaQuery.where(predicate);
return em.createQuery(criteriaQuery).getResultList();
}
}
In the last step in this section, we are creating GraphQL resources. In short, all we need to do is to annotate the class with @GraphQLApi
and methods with @Query
or @Mutation
. If you define any input parameter in the method you should annotate it with @Name
. The class EmployeeFetcher
is responsible just for defining queries. It uses built-in methods provided by PanacheRepository
and our custom search method created inside the EmployeeRepository
class.
@GraphQLApi
public class EmployeeFetcher {
private EmployeeRepository repository;
public EmployeeFetcher(EmployeeRepository repository){
this.repository = repository;
}
@Query("employees")
public List<Employee> findAll() {
return repository.listAll();
}
@Query("employee")
public Employee findById(@Name("id") Long id) {
return repository.findById(id);
}
@Query("employeesWithFilter")
public List<Employee> findWithFilter(@Name("filter") EmployeeFilter filter) {
return repository.findByCriteria(filter);
}
}
4. Fetching Relations with Quarkus GraphQL
As you probably figured out, all the JPA relations are configured in a lazy mode. To fetch them we should explicitly set such a request in our GraphQL query. For example, we may query all departments and fetch organization to each of the departments returned on the list. Let’s analyze the request visible below. It contains field organization related to the @ManyToOne
relation between Department
and Organization
entities.
{
departments {
id
name
organization {
id
name
}
}
}
How to handle it on the server-side? Firstly, we need to detect the existence of such a relationship field in our GraphQL query. In order to analyze the input query, we can use DataFetchingEnvironment
and DataFetchingFieldSelectionSet
objects. Then we need to prepare different JPA queries depending on the parameters set in the GraphQL query. Once again, we will use JPA Criteria for that. The same as before, we place implementation responsible for performing a dynamic join inside the repository bean. Also, to obtain DataFetchingEnvironment
we first need to inject GraphQL Context
bean. With the following DepartmentRepository
implementation, we are avoiding possible N+1 problem, and fetching only the required relation.
@ApplicationScoped
public class DepartmentRepository implements PanacheRepository<Department> {
private EntityManager em;
private Context context;
public DepartmentRepository(EntityManager em, Context context) {
this.em = em;
this.context = context;
}
public List<Department> findAllByCriteria() {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Department> criteriaQuery = builder.createQuery(Department.class);
Root<Department> root = criteriaQuery.from(Department.class);
DataFetchingEnvironment dfe = context.unwrap(DataFetchingEnvironment.class);
DataFetchingFieldSelectionSet selectionSet = dfe.getSelectionSet();
if (selectionSet.contains("employees")) {
root.fetch("employees", JoinType.LEFT);
}
if (selectionSet.contains("organization")) {
root.fetch("organization", JoinType.LEFT);
}
criteriaQuery.select(root).distinct(true);
return em.createQuery(criteriaQuery).getResultList();
}
public Department findByIdWithCriteria(Long id) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Department> criteriaQuery = builder.createQuery(Department.class);
Root<Department> root = criteriaQuery.from(Department.class);
DataFetchingEnvironment dfe = context.unwrap(DataFetchingEnvironment.class);
DataFetchingFieldSelectionSet selectionSet = dfe.getSelectionSet();
if (selectionSet.contains("employees")) {
root.fetch("employees", JoinType.LEFT);
}
if (selectionSet.contains("organization")) {
root.fetch("organization", JoinType.LEFT);
}
criteriaQuery.where(builder.equal(root.get("id"), id));
return em.createQuery(criteriaQuery).getSingleResult();
}
}
Finally, we just need to create a resource controller. It uses our custom JPA queries defined in DepartmentRepository
.
@GraphQLApi
public class DepartmentFetcher {
private DepartmentRepository repository;
DepartmentFetcher(DepartmentRepository repository) {
this.repository = repository;
}
@Query("departments")
public List<Department> findAll() {
return repository.findAllByCriteria();
}
@Query("department")
public Department findById(@Name("id") Long id) {
return repository.findByIdWithCriteria(id);
}
}
5. Handling GraphQL Mutations with Quarkus
In our sample application, we separate the implementation of queries from mutations. So, let’s take a look at the DepartmentMutation
class. Instead of @Query
we use @Mutation
annotation on the method. We also use the DepartmentInput
object as a mutation method parameter.
@GraphQLApi
public class DepartmentMutation {
private DepartmentRepository departmentRepository;
private OrganizationRepository organizationRepository;
DepartmentMutation(DepartmentRepository departmentRepository,
OrganizationRepository organizationRepository) {
this.departmentRepository = departmentRepository;
this.organizationRepository = organizationRepository;
}
@Mutation("newDepartment")
public Department newDepartment(@Name("input") DepartmentInput departmentInput) {
Organization organization = organizationRepository
.findById(departmentInput.getOrganizationId());
Department department = new Department(null, departmentInput.getName(), null, organization);
departmentRepository.persist(department);
return department;
}
}
6. Testing with GraphiQL
Once we finished the implementation we may build and start our Quarkus application using the following Maven command.
$ mvn package quarkus:dev
After startup, the application is available on 8080 port. We may also take a look at the list of included Quarkus modules.
Quarkus automatically generates a GraphQL schema based on the source code. In order to display it, you should invoke the URL http://localhost:8080/graphql/schema.graphql
. Of course, it is an optional step. But something that will be pretty useful for us is the GraphiQL tool. It is embedded into the Quarkus application. It allows us to easily interact with GraphQL APIs and can be accessed from http://localhost:8080/graphql-ui/. First, let’s run the following query that tests a filtering feature.
{
employeesWithFilter(filter: {
salary: {
operator: "gt"
value: "19000"
},
age: {
operator: "gt"
value: "30"
}
}) {
id
firstName
lastName
position
}
}
Here’s the SQL query generated by Hibernate for our GraphQL query.
select
employee0_.id as id1_1_,
employee0_.age as age2_1_,
employee0_.department_id as departme7_1_,
employee0_.firstName as firstnam3_1_,
employee0_.lastName as lastname4_1_,
employee0_.organization_id as organiza8_1_,
employee0_.position as position5_1_,
employee0_.salary as salary6_1_
from
Employee employee0_
where
employee0_.salary>19000
and employee0_.age>30
Our sample application inserts some test data to the H2 database on startup. So, we just need to execute a query using GraphiQL.
Now, let’s repeat the same exercise to test the join feature. Here’s our GraphQL input query responsible for fetching relation with the Organization
entity.
{
department(id: 5) {
id
name
organization {
id
name
}
}
}
Hibernate generates the following SQL query for that.
select
department0_.id as id1_0_0_,
organizati1_.id as id1_2_1_,
department0_.name as name2_0_0_,
department0_.organization_id as organiza3_0_0_,
organizati1_.name as name2_2_1_
from
Department department0_
left outer join
Organization organizati1_
on department0_.organization_id=organizati1_.id
where
department0_.id=5
Once again, let’s view the response for our query using GraphiQL.
Final Thoughts
GraphQL support in Quarkus, like several other features, is based on the SmallRye project. In Spring Boot, we can use third-party libraries that provide GraphQL support. One of them is Netflix DGS. There is also a popular Kickstart GraphQL library described in this article. However, we can’t use any default implementation developed by the Spring Team.
With Quarkus GraphQL support we can easily migrate from Spring Boot to Quarkus. I would not say that currently, Quarkus offers many GraphQL features like for example Netflix DGS, but it is still under active development. We could also easily replace Spring Data with the Quarkus Panache project. The lack of some features similar to the Spring Data Specification
, could be easily bypassed by using JPA CriteriaBuilder
and EntityManager
directly. Finally, I really like the Quarkus GraphQL support, because I don’t have to take care of the GraphQL schema creation, which is generated automatically basing on the source code and annotations.
8 COMMENTS