In this article, you will learn how to use the Standard JPA Criteria API (build queries programmatically) for generating more advanced queries with filters and joins in Spring for GraphQL.
In the previous article, we learn how to set up a GraphQL server using Spring Boot.
Source Code
You need to clone my GitHub repository. Firstly, you should go to the advanced-graphql
directory. Our sample Spring Boot exposes API over GraphQL and connects to the in-memory H2 database. It uses Spring Data JPA as a layer to interact with the database. There are three entities Employee, Department and Organization
. Each of them is stored in a separate table. Here’s a relationship model.
Getting started with Spring for GraphQL
In addition to the standard Spring Boot modules we need to include the following two dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
And also enable the GraphiQL interface (default is false)
spring:
graphql:
graphiql:
enabled: true
Let’s GraphQL schema for the Department
entity. The Department
type references the two other types: Organization
and Employee
(the list of employees). There are two queries for searching all departments and a department by id, and a single mutation for adding a new department.
type Query {
departments: [Department]
department(id: ID!): Department!
}
type Mutation {
newDepartment(department: DepartmentInput!): Department
}
input DepartmentInput {
name: String!
organizationId: Int
}
type Department {
id: ID!
name: String!
organization: Organization
employees: [Employee]
}
The Organization
type schema is pretty similar. From the more advanced stuff, we need to handle joins to the Employee
and Department
types.
extend type Query {
organizations: [Organization]
organization(id: ID!): Organization!
}
extend type Mutation {
newOrganization(organization: OrganizationInput!): Organization
}
input OrganizationInput {
name: String!
}
type Organization {
id: ID!
name: String!
employees: [Employee]
departments: [Department]
}
And the last schema – for the Employee
type. Unlike the previous schemas, it defines the type responsible for handling filtering. The EmployeeFilter is able to filter by salary, position, or age. There is also the query method for handling filtering – employeesWithFilter
.
extend type Query {
employees: [Employee]
employeesWithFilter(filter: EmployeeFilter): [Employee]
employee(id: ID!): Employee!
}
extend type Mutation {
newEmployee(employee: EmployeeInput!): Employee
}
input EmployeeInput {
firstName: String!
lastName: String!
position: String!
salary: Int
age: Int
organizationId: Int!
departmentId: Int!
}
type Employee {
id: ID!
firstName: String!
lastName: String!
position: String!
salary: Int
age: Int
department: Department
organization: Organization
}
input EmployeeFilter {
salary: FilterField
age: FilterField
position: FilterField
}
input FilterField {
operator: String!
value: String!
}
Create Entities
Here’s the Employee
entity corresponding to the Employee
type defined in GraphQL schema.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@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;
}
Here we have the Department
entity
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
private Integer id;
private String name;
@OneToMany(mappedBy = "department")
private Set<Employee> employees;
@ManyToOne(fetch = FetchType.LAZY)
private Organization organization;
}
Finally, we have the Organization
entity.
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Organization {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
private Integer id;
private String name;
@OneToMany(mappedBy = "organization")
private Set<Department> departments;
@OneToMany(mappedBy = "organization")
private Set<Employee> employees;
}
Using GraphQL for Spring with Spring Boot
Spring for GraphQL provides an annotation-based programming model using the well-known @Controller pattern. I will use the standard JPA Criteria API for generating more advanced queries
with filters and joins.
We need to annotate query methods with the @QueryMapping, and mutation methods with @MutationMapping. The last query method employeesWithFilter
performs advanced filtering
based on the dynamic list of fields passed in the input EmployeeFilter type. To pass an input parameter we should annotate the method argument with @Argument.
@Controller
public class EmployeeController {
DepartmentRepository departmentRepository;
EmployeeRepository employeeRepository;
OrganizationRepository organizationRepository;
EmployeeController(DepartmentRepository departmentRepository,
EmployeeRepository employeeRepository,
OrganizationRepository organizationRepository) {
this.departmentRepository = departmentRepository;
this.employeeRepository = employeeRepository;
this.organizationRepository = organizationRepository;
}
@QueryMapping
public Iterable<Employee> employees() {
return employeeRepository.findAll();
}
@QueryMapping
public Employee employee(@Argument Integer id) {
return employeeRepository.findById(id).orElseThrow();
}
@MutationMapping
public Employee newEmployee(@Argument EmployeeInput employee) {
Department department = departmentRepository
.findById(employee.getDepartmentId()).get();
Organization organization = organizationRepository
.findById(employee.getOrganizationId()).get();
return employeeRepository.save(new Employee(null, employee.getFirstName(), employee.getLastName(),
employee.getPosition(), employee.getAge(), employee.getSalary(),
department, organization));
}
@QueryMapping
public Iterable<Employee> employeesWithFilter(
@Argument EmployeeFilter filter) {
Specification<Employee> spec = null;
if (filter.getSalary() != null)
spec = bySalary(filter.getSalary());
if (filter.getAge() != null)
spec = (spec == null ? byAge(filter.getAge()) : spec.and(byAge(filter.getAge())));
if (filter.getPosition() != null)
spec = (spec == null ? byPosition(filter.getPosition()) :
spec.and(byPosition(filter.getPosition())));
if (spec != null)
return employeeRepository.findAll(spec);
else
return employeeRepository.findAll();
}
private Specification<Employee> bySalary(FilterField filterField) {
return (root, query, builder) -> filterField
.generateCriteria(builder, root.get("salary"));
}
private Specification<Employee> byAge(FilterField filterField) {
return (root, query, builder) -> filterField
.generateCriteria(builder, root.get("age"));
}
private Specification<Employee> byPosition(FilterField filterField) {
return (root, query, builder) -> filterField
.generateCriteria(builder, root.get("position"));
}
}
In order to use JPA Criteria API we need it needs to extend the JpaSpecificationExecutor
interface. The same rule applies to both others DepartmentRepository
and OrganizationRepository
.
public interface EmployeeRepository extends
CrudRepository<Employee, Integer>, JpaSpecificationExecutor<Employee> {
}
Now, let’s switch to another controller. Here’s the implementation of DepartmentController
. It shows the example of relationship fetching. We use DataFetchingEnvironment
to detect if the input query contains a relationship
field. In our case, it may be employees
or organization
. If any of those fields is defined we add the particular relation to the JOIN statement.
@Controller
public class DepartmentController {
DepartmentRepository departmentRepository;
OrganizationRepository organizationRepository;
DepartmentController(DepartmentRepository departmentRepository, OrganizationRepository organizationRepository) {
this.departmentRepository = departmentRepository;
this.organizationRepository = organizationRepository;
}
@MutationMapping
public Department newDepartment(@Argument DepartmentInput department) {
Organization organization = organizationRepository
.findById(department.getOrganizationId()).get();
return departmentRepository.save(new Department(null, department.getName(), null, organization));
}
@QueryMapping
public Iterable<Department> departments(DataFetchingEnvironment environment) {
DataFetchingFieldSelectionSet s = environment.getSelectionSet();
List<Specification<Department>> specifications = new ArrayList<>();
if (s.contains("employees") && !s.contains("organization"))
return departmentRepository.findAll(fetchEmployees());
else if (!s.contains("employees") && s.contains("organization"))
return departmentRepository.findAll(fetchOrganization());
else if (s.contains("employees") && s.contains("organization"))
return departmentRepository.findAll(fetchEmployees().and(fetchOrganization()));
else
return departmentRepository.findAll();
}
@QueryMapping
public Department department(@Argument Integer id, DataFetchingEnvironment environment) {
Specification<Department> spec = byId(id);
DataFetchingFieldSelectionSet selectionSet = environment
.getSelectionSet();
if (selectionSet.contains("employees"))
spec = spec.and(fetchEmployees());
if (selectionSet.contains("organization"))
spec = spec.and(fetchOrganization());
return departmentRepository.findOne(spec).orElseThrow(NoSuchElementException::new);
}
private Specification<Department> fetchOrganization() {
return (root, query, builder) -> {
Fetch<Department, Organization> f = root
.fetch("organization", JoinType.LEFT);
Join<Department, Organization> join = (Join<Department, Organization>) f;
return join.getOn();
};
}
private Specification<Department> fetchEmployees() {
return (root, query, builder) -> {
Fetch<Department, Employee> f = root
.fetch("employees", JoinType.LEFT);
Join<Department, Employee> join = (Join<Department, Employee>) f;
return join.getOn();
};
}
private Specification<Department> byId(Integer id) {
return (root, query, builder) -> builder.equal(root.get("id"), id);
}
}
Here’s the OrganizationController
implementation
@Controller
public class OrganizationController {
OrganizationRepository repository;
OrganizationController(OrganizationRepository repository) {
this.repository = repository;
}
@MutationMapping
public Organization newOrganization(@Argument OrganizationInput organization) {
return repository.save(new Organization(null, organization.getName(), null, null));
}
@QueryMapping
public Iterable<Organization> organizations() {
return repository.findAll();
}
@QueryMapping
public Organization organization(@Argument Integer id, DataFetchingEnvironment environment) {
Specification<Organization> spec = byId(id);
DataFetchingFieldSelectionSet selectionSet = environment
.getSelectionSet();
if (selectionSet.contains("employees"))
spec = spec.and(fetchEmployees());
if (selectionSet.contains("departments"))
spec = spec.and(fetchDepartments());
return repository.findOne(spec).orElseThrow();
}
private Specification<Organization> fetchDepartments() {
return (root, query, builder) -> {
Fetch<Organization, Department> f = root
.fetch("departments", JoinType.LEFT);
Join<Organization, Department> join = (Join<Organization, Department>) f;
return join.getOn();
};
}
private Specification<Organization> fetchEmployees() {
return (root, query, builder) -> {
Fetch<Organization, Employee> f = root
.fetch("employees", JoinType.LEFT);
Join<Organization, Employee> join = (Join<Organization, Employee>) f;
return join.getOn();
};
}
private Specification<Organization> byId(Integer id) {
return (root, query, builder) -> builder.equal(root.get("id"), id);
}
}
Create Unit Tests
Here are the test for the Employee @Controller
. We need to annotate the whole test class with @AutoConfigureGraphQlTester
.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureGraphQlTester
public class EmployeeControllerTests {
@Autowired
private GraphQlTester tester;
@Test
void addEmployee() {
String query = "mutation { newEmployee(employee: { firstName: \"John\" lastName: \"Wick\" position: \"developer\" salary: 10000 age: 20 departmentId: 1 organizationId: 1}) { id } }";
Employee employee = tester.document(query)
.execute()
.path("data.newEmployee")
.entity(Employee.class)
.get();
Assertions.assertNotNull(employee);
Assertions.assertNotNull(employee.getId());
}
@Test
void findAll() {
String query = "{ employees { id firstName lastName salary } }";
List<Employee> employees = tester.document(query)
.execute()
.path("data.employees[*]")
.entityList(Employee.class)
.get();
Assertions.assertTrue(employees.size() > 0);
Assertions.assertNotNull(employees.get(0).getId());
Assertions.assertNotNull(employees.get(0).getFirstName());
}
@Test
void findById() {
String query = "{ employee(id: 1) { id firstName lastName salary } }";
Employee employee = tester.document(query)
.execute()
.path("data.employee")
.entity(Employee.class)
.get();
Assertions.assertNotNull(employee);
Assertions.assertNotNull(employee.getId());
Assertions.assertNotNull(employee.getFirstName());
}
@Test
void findWithFilter() {
String query = "{ employeesWithFilter(filter: { salary: { operator: \"gt\" value: \"12000\" } }) { id firstName lastName salary } }";
List<Employee> employees = tester.document(query)
.execute()
.path("data.employeesWithFilter[*]")
.entityList(Employee.class)
.get();
Assertions.assertTrue(employees.size() > 0);
Assertions.assertNotNull(employees.get(0).getId());
Assertions.assertNotNull(employees.get(0).getFirstName());
}
}
For the Deparment
type are very similar. Additionally, we need to test join statements
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureGraphQlTester
public class DepartmentControllerTests {
@Autowired
private GraphQlTester tester;
@Test
void addDepartment() {
String query = "mutation { newDepartment(department: { name: \"Test10\" organizationId: 1}) { id } }";
Department department = tester.document(query)
.execute()
.path("data.newDepartment")
.entity(Department.class)
.get();
Assertions.assertNotNull(department);
Assertions.assertNotNull(department.getId());
}
@Test
void findAll() {
String query = "{ departments { id name } }";
List<Department> departments = tester.document(query)
.execute()
.path("data.departments[*]")
.entityList(Department.class)
.get();
Assertions.assertTrue(departments.size() > 0);
Assertions.assertNotNull(departments.get(0).getId());
Assertions.assertNotNull(departments.get(0).getName());
}
@Test
void findById() {
String query = "{ department(id: 1) { id name organization { id } } }";
Department department = tester.document(query)
.execute()
.path("data.department")
.entity(Department.class)
.get();
Assertions.assertNotNull(department);
Assertions.assertNotNull(department.getId());
Assertions.assertNotNull(department.getOrganization());
Assertions.assertNotNull(department.getOrganization().getId());
}
}
Testing with GraphiQL
We can easily start the application and you can access the GraphiQL tool under the address http://localhost:8080/graphql
. I use Postman tool to access the GraphiQL