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:

  1. The annotation processor that generates a metamodel used to compose predicates that can be interpreted by The Stream renderer.

  2. 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.