How JPAstreamer Works
JPAstreamer is a library that enriches the API of an existing JPA provider to include Java Streams as a way to express queries. To do so, JPAstreamer consists of two key parts:
-
The annotation processor that generates a metamodel used to compose predicates that can be interpreted by The Stream renderer.
-
The Stream renderer that inspects the Stream pipelines and renders optimized JPA queries.
This section describes how both of these components operate and why they are essential.
The annotation processor
JPAstreamer requires its own metamodel of the JPA entity beans to render the Java Streams to JPA queries. The metamodel is automatically generated by an annotation processor that operates at compile time as soon as JPAstreamer has been installed. The annotation processor inspects all classes annotated with @Entity
, e.g. Foo.class and generates an equivalent Foo$
that represents every column as a Field
which can be used to obtain predicates. Let’s have a closer look at the metamodel.
JPAstreamer's metamodel
The metamodel contains a representation of all @Entity
-beans in the application. This means, an @Entity
-bean named Film
will have a corresponding generated class named Film$
as below:
@Entity
@Table(name = "film", schema = "sakila")
public class Film {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "film_id", nullable = false, updatable = false, columnDefinition = "smallint(5)")
private Integer filmId;
@Column(name = "title", nullable = false, columnDefinition = "varchar(255)")
private String title;
// ...
}
Will be represented in the metamodel as:
public class Film$ {
public static final ComparableField<Film, Integer> filmId = ComparableField.create(
Film.class, (1)
"filmId", (2)
Film::getFilmId, (3)
false (4)
);
public static final StringField<Film> title = StringField.create(
Film.class,
"title",
Film::getTitle,
false
);
// ...
}
1 | Declares which table the Field belongs to |
2 | The name of the field |
3 | A reference to the associated getter |
4 | A boolean that describes if the Field is nullable |
Having access to these fields, operators can be applied which returns a Predicate<T>
that can be evaluated by The Stream renderer. For example:
long nrOfFilmsStartingWithA = jpaStreamer.stream(Film.class)
.filter(Film$.title.startsWith("A")) "(1)
.count();
#<1> As title
is of type StringField
we get access to operators such as startsWith
that returns a Predicate<T>
that evaluates to true
or false
.
You can read more about the use of fields and predicates here.
The metamodel is stored in the projects target-folder and does not need to be checked-in with your source code, nor needs testing. |
The Stream renderer
Most JPA providers, by default, has the ability to provide the query result as a Stream
. That makes it possible to e.g. select all rows of a table and then use Stream operators to narrow the results. Although, that process requires every object of the table to be materialized by the JVM, which is not desired as it impedes the performance.
As Stream
is merely a Java interface, JPAstreamer if free to use a complete custom implementation of that interface. By doing so, JPAstreamer can render what seems like ordinary Streams to optimized JPA queries. The renderer inspects the Stream pipeline and translates the Stream operators to JPA constructs as shown below:
jpaStreamer.stream(Film.class)
.filter(Film$.rating.equal("G").and(Film$.length.greaterThan(100)))
.sorted(Film$.length.reversed().thenComparing(Film$.title.comparator()))
.skip(10)
.limit(5)
.forEach(System.out::println);
This Stream is rendered to the following JPA query when executed:
select
film0_.film_id as film_id1_0_,
film0_.description as descript2_0_,
film0_.last_update as last_upd3_0_,
film0_.length as length4_0_,
film0_.rating as rating5_0_,
film0_.rental_duration as rental_d6_0_,
film0_.rental_rate as rental_r7_0_,
film0_.replacement_cost as replacem8_0_,
film0_.special_features as special_9_0_,
film0_.title as title10_0_
from (1)
film film0_
where (2)
film0_.rating=?
and film0_.length>100
order by (3)
film0_.length desc,
film0_.title asc limit ?,
?
1 | Corresponds to stream(Film.class) |
2 | Corresponds to filter() |
3 | Corresponds to sort() |
This way we obtain the expressiveness of the Stream API without compromising the performance of the queries.
For JPAstreamer to render optimized queries you must use the generated fields shown in JPAstreamer's metamodel. Read more about this in the chapter JPAstreamer Predicates. |