Express JPA Queries as Java Streams
In this article, you will learn how to use the JPAstreamer library to express your JPA queries with Java streams. I will also show you how to integrate this library with Spring Boot and Spring Data. The idea around it is very simple but at the same time brilliant. The library creates a SQL query based on your Java stream. That’s all. I have already mentioned this library on my Twitter account.
Before we start, let’s take a look at the following picture. It should explain the concept in a simple way. That’s pretty intuitive, right?
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. Then you should just follow my instructions. Let’s begin.
Dependencies and configuration
As an example, we have a simple Spring Boot application that runs an embedded H2 database and exposes data through a REST API. It also uses Spring Data JPA to interact with the database. But with the JPAstreamer
library, this is completely transparent for us. So, in the first step, we need to include the following two dependencies. The first of them adds JPAstreamer
while the second integrate it with Spring Boot.
<dependency>
<groupId>com.speedment.jpastreamer</groupId>
<artifactId>jpastreamer-core</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>com.speedment.jpastreamer.integration.spring</groupId>
<artifactId>spring-boot-jpastreamer-autoconfigure</artifactId>
<version>1.0.1</version>
</dependency>
Then, we need to add Spring Boot Web and JPA starters, H2 database, and optionally Lombok.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
I’m using Java 15 for compilation. Because I use Java records for DTO I need to enable preview features. Here’s the plugin responsible for it.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>15</release>
<compilerArgs>
--enable-preview
</compilerArgs>
<source>15</source>
<target>15</target>
</configuration>
</plugin>
The JPAstreamer
library generates source code based on your entity model. Then we may use it, for example, to perform filtering or sorting. But we will talk about it in the next part of the article. For now, let’s configure the build process with build-helper-maven-plugin
. It generates the source code in the target/generated-sources/annotations
directory. If you use IntelliJ it is automatically included as a source folder in your project.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/annotations</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
Here is a source code generated by JPAstreamer
for our entity model.
Entity model for JPA
Let’s take a look at our example entities. Here’s the Employee
class. Each employee is assigned to the department and organization.
@Entity
@NoArgsConstructor
@Getter
@Setter
@ToString
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String position;
private int salary;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
@ManyToOne(fetch = FetchType.LAZY)
private Organization organization;
}
Here’s the Department
entity.
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@OneToMany(mappedBy = "department")
private Set<Employee> employees;
@ManyToOne(fetch = FetchType.LAZY)
private Organization organization;
}
Here’s the Organization
entity.
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Organization {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@OneToMany(mappedBy = "organization")
private Set<Department> departments;
@OneToMany(mappedBy = "organization")
private Set<Employee> employees;
}
We will also use Java records to create DTOs. Here’s a simple DTO for the Employee
entity.
public record EmployeeDTO(
Integer id,
String name,
String position,
int salary
) {
public EmployeeDTO(Employee emp) {
this(emp.getId(), emp.getName(), emp.getPosition(), emp.getSalary());
}
}
We also have a DTO record to express relationship fields.
public record EmployeeWithDetailsDTO(
Integer id,
String name,
String position,
int salary,
String organizationName,
String departmentName
) {
public EmployeeWithDetailsDTO(Employee emp) {
this(emp.getId(), emp.getName(), emp.getPosition(), emp.getSalary(),
emp.getOrganization().getName(),
emp.getDepartment().getName());
}
}
Express JPA queries as Java streams
Let’s begin with a simple example. We would like to get all the departments, sort it ascending based on the name
field, and then convert it to DTO. We just need to get an instance of JPAstreamer
object and invoke a stream()
method. Then you do everything else as you would act with standard Java streams.
@GetMapping
public List<DepartmentDTO> findAll() {
return streamer.stream(Department.class)
.sorted(Department$.name)
.map(DepartmentDTO::new)
.collect(Collectors.toList());
}
Now, we can call the endpoint after starting our Spring Boot application.
$ curl http://localhost:8080/departments
[{"id":4,"name":"aaa"},{"id":3,"name":"bbb"},{"id":2,"name":"ccc"},{"id":1,"name":"ddd"}]
Let’s take a look at something a little bit more advanced. We are going to find employees with salaries greater than an input value, sort them by salaries, and of course map to DTO.
@GetMapping("/greater-than/{salary}")
public List<EmployeeDTO> findBySalaryGreaterThan(@PathVariable("salary") int salary) {
return streamer.stream(Employee.class)
.filter(Employee$.salary.greaterThan(salary))
.sorted(Employee$.salary)
.map(EmployeeDTO::new)
.collect(Collectors.toList());
}
Then, we call another endpoint once again.
$ curl http://localhost:8080/employees/greater-than/25000
[{"id":5,"name":"Test5","position":"Architect","salary":30000},{"id":7,"name":"Test7","position":"Manager","salary":30000},{"id":9,"name":"Test9","position":"Developer","salary":30000}]
We can also perform JPA pagination operations by using skip
and limit
Java streams methods.
@GetMapping("/offset/{offset}/limit/{limit}")
public List<EmployeeDTO> findAllWithPagination(
@PathVariable("offset") int offset,
@PathVariable("limit") int limit) {
return streamer.stream(Employee.class)
.skip(offset)
.limit(limit)
.map(EmployeeDTO::new)
.collect(Collectors.toList());
}
What is important all such operations are performed on the database side. Here’s the SQL query generated for the implementation visible above.
What about relationships between entities? Of course relationships between tables are handled via the JPA provider. In order to perform the JOIN
operation with JPAstreamer
, we just need to specify the joining stream. By default, it is LEFT JOIN, but we can customize it when calling the joining()
method. In the following fragment of code, we join Department
and Organization
, which are in @ManyToOne
relationship with the Employee
entity.
@GetMapping("/{id}")
public EmployeeWithDetailsDTO findById(@PathVariable("id") Integer id) {
return streamer.stream(of(Employee.class)
.joining(Employee$.department)
.joining(Employee$.organization))
.filter(Employee$.id.equal(id))
.map(EmployeeWithDetailsDTO::new)
.findFirst()
.orElseThrow();
}
Of course, we can call many other Java stream methods. In the following fragment of code, we count the number of employees assigned to the particular department.
@GetMapping("/{id}/count-employees")
public long getNumberOfEmployees(@PathVariable("id") Integer id) {
return streamer.stream(Department.class)
.filter(Department$.id.equal(id))
.map(Department::getEmployees)
.mapToLong(Set::size)
.sum();
}
And the last example today. We get all the employees assigned to a particular department and map each of them to EmployeeDTO
.
@GetMapping("/{id}/employees")
public List<EmployeeDTO> getEmployees(@PathVariable("id") Integer id) {
return streamer.stream(Department.class)
.filter(Department$.id.equal(id))
.map(Department::getEmployees)
.flatMap(Set::stream)
.map(EmployeeDTO::new)
.collect(Collectors.toList());
}
Integration with Spring Boot
We can easily integrate JPAstreamer
with Spring Boot and Spring Data JPA. In fact, you don’t have anything more than just include a dependency responsible for integration with Spring. It provides auto-configuration also for Spring Data JPA. Therefore, we just need to inject the JPAStreamer
bean into the target service or controller.
@RestController
@RequestMapping("/employees")
public class EmployeeController {
private final JPAStreamer streamer;
public EmployeeController(JPAStreamer streamer) {
this.streamer = streamer;
}
@GetMapping("/greater-than/{salary}")
public List<EmployeeDTO> findBySalaryGreaterThan(
@PathVariable("salary") int salary) {
return streamer.stream(Employee.class)
.filter(Employee$.salary.greaterThan(salary))
.sorted(Employee$.salary)
.map(EmployeeDTO::new)
.collect(Collectors.toList());
}
// ...
}
Final Thoughts
The concept around the JPAstreamer
library seems to be very interesting. I really like it. The only disadvantage I found is that it sends certain data back to Speedment’s servers for Google Analytics. If you wish to disable this feature, you need to contact their team. To be honest, I’m concerned a little. But it doesn’t change my point of view, that JPAstreamer
is a very useful library. If you are interested in topics related to Java streams and collections you may read my article Using Eclipse Collections.
21 COMMENTS