Blog logo on a black background with the words "Discover What's Next in Tech!"

JPA queries as Java streams with JPA Streamer

JPA Streamer is a Java library for applications that use JPA. It can be used to perform database operations with standard Java stream operations such as map, filter or sort.

How does JPA Streamer work?

JPA Streamer uses annotation processing to generate a meta-model at compile time for all classes marked with @Entity, which can be interpreted and used by the query optimizer of JPA Streamer.

What does this look like in the code?

Our sample entity book (getter/setter not included):

@Entity(name = "t_book")
public class Book {
    private static final String SEQ_GENERATOR = "SEQ_BOOK_ID";
    private static final String SEQUENCE = "SEQ_BOOK";

    @Id
    @GeneratedValue(generator = SEQ_GENERATOR)
    @SequenceGenerator(name = SEQ_GENERATOR, sequenceName = SEQUENCE, allocationSize = 1)
    @Column(name = "book_id")
    private long id;
    @Column(name = "title")
    private String title;

    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Page> pages;

    @Column(name = "page_count", nullable = false)
    private int pageCount;

    @Column(name = "book_character")
    @ManyToMany(cascade = { CascadeType.ALL })
    @JoinTable(
            name = "t_book_book_character",
            joinColumns = { @JoinColumn(name = "book_id") },
            inverseJoinColumns = { @JoinColumn(name = "book_character_id") }
    )
    private List<BookCharacter> bookCharacters = new ArrayList<>();

}

The class receives an additional Book$ from the JPA streamer:

public final class Book$ {
   
    /**
     * This Field corresponds to the {@link Book} field "pages".
     */
    public static final ReferenceField<Book, List<Page>> pages = ReferenceField.create(
        Book.class,
        "pages",
        Book::getPages,
        false
    );
    /**
     * This Field corresponds to the {@link Book} field "id".
     */
    public static final LongField<Book> id = LongField.create(
        Book.class,
        "id",
        Book::getId,
        false
    );
    /**
     * This Field corresponds to the {@link Book} field "pageCount".
     */
    public static final IntField<Book> pageCount = IntField.create(
        Book.class,
        "pageCount",
        Book::getPageCount,
        false
    );
    /**
     * This Field corresponds to the {@link Book} field "title".
     */
    public static final StringField<Book> title = StringField.create(
        Book.class,
        "title",
        Book::getTitle,
        false
    );
    /**
     * This Field corresponds to the {@link Book} field "bookCharacters".
     */
    public static final ReferenceField<Book, List<BookCharacter>> bookCharacters = ReferenceField.create(
        Book.class,
        "bookCharacters",
        Book::getBookCharacters,
        false
    );
}

What is the benefit of this?

This Book$ class can now be used to utilize JPA queries via streaming API. This can help to write queries faster and easier.

Let's start with a simple example: We want to find out how many book titles begin with "G".

The SQL could look like this:

SELECT COUNT(BOOK_ID) FROM T_BOOK WHERE TITLE LIKE 'G%';

Implementation with the CriteriaBuilder (Hibernate provides the implementation):

CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class);
Root<Book> bookRoot = criteriaQuery.from(Book.class);
criteriaQuery.select(criteriaBuilder.count(bookRoot));
criteriaQuery.where(criteriaBuilder.like(bookRoot.get("title"), "G%"));

Long count = em.createQuery(criteriaQuery).getSingleResult();

The same example with the JPA streamer library:

JPAStreamer streamer = JPAStreamer.of(em.getEntityManagerFactory());

long count = streamer.stream(Book.class)
        .filter(Book$.title.startsWith("G"))
        .count();

Please note that the class generated by JPA Streamer is used in the filter, otherwise the whole thing will not work.

In both cases, the following query is generated during execution:

    select
        count(b1_0.book_id)
    from
        t_book b1_0
    where
        b1_0.title like ?

However, the JPA streamer example is much leaner and much easier to understand.

Another, somewhat more complex example of projections (projections are always used when not all fields are required): Instead of books, we only want to select the book titles that start with D. The list should be sorted.

The SQL for this:

SELECT TITLE FROM T_BOOK WHERE TITLE LIKE 'D%' ORDER BY TITLE;

Here again the implementation with the CriteriaBuilder:

CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<String> criteriaQuery = criteriaBuilder.createQuery(String.class);
Root<Book> bookRoot = criteriaQuery.from(Book.class);

criteriaQuery.select(bookRoot.get("title"));
criteriaQuery.where(criteriaBuilder.like(bookRoot.get("title"), "D%"));
criteriaQuery.orderBy(criteriaBuilder.asc(bookRoot.get("title")));

List<String> bookTitlesSorted = em.createQuery(criteriaQuery).getResultList();

And the implementation with JPA Streamer:

StreamConfiguration<Book> bookTitleConfiguration = StreamConfiguration.of(Book.class)
        .selecting(Projection.select(Book$.title)); 
streamer.stream(bookTitleConfiguration)
        .sorted(Book$.title)
        .filter(Book$.title.startsWith("D"))
        .map(Book::getTitle)
        .collect(Collectors.toList());

In both cases, the generated queries are identical to our SQL statement:

    select
        b1_0.title
    from
        t_book b1_0
    where
        b1_0.title like ?
    order by
        1

As before, the JPA Streamer code reads somewhat more simply than its CriteriaBuilder counterpart.

What else can the JPA Streamer library do?

JPA Streamer supports a variety of database operations. A brief overview:

SQLJava Stream
FROMstream()
SELECTmap(Projection.select())
WHEREfilter() (before collecting)
ORDER BYsorted()
OFFSETskip()
LIMITlimit()
COUNTcount()
GROUP BYcollect(groupingBY())
HAVINGfilter() (after collecting)
DISTINCTdistinct()
SELECTmap()
UNIONconcat(s0, s1).distinct()
JOINflatmap()

Further information on the individual operations can be found in the official documentation. In the Github project you will find further examples.

Is the project worthwhile?

JPA Streamer is a powerful library that can simplify the development of JPA applications. The ability to write queries using standard Java stream operators can help make writing queries faster and easier. In addition, the queries are usually easier to read and understand. However, as with the CriteriaBuilder and SQL queries, readability and simplicity also have their limits with increasing complexity. In addition, functions are currently still missing or not all operations are currently supported. The use in the project must therefore always be evaluated individually.

Sources:

https://quarkus.io/blog/jpastreamer-extension

You are currently viewing a placeholder content from YouTube. To access the actual content, click the button below. Please note that doing so will share data with third-party providers.

More Information
https://speedment.github.io/jpa-streamer/jpa-streamer/latest/fetching-data/sql-equivalents.html
https://github.com/speedment/jpa-streamer-demo

 

Learn more about Java programming

Denis Peters

About ME

Denis Peters is a state-certified computer scientist and works at doubleSlash as a software architect. He has extensive knowledge in the conception and development of web applications and has the Postfinder from Deutsche Post AG co-developed. Denis Peters focuses on application support and quality assurance, among other things.

All contributions from Denis Peters

Learn more

Further information on our website and in our newsletter

Arrow up