JPA Abfragen als Java-Streams mit JPA-Streamer

08.04.2024

JPA-Streamer ist eine Java-Bibliothek für Applikationen mit JPA-Einsatz. Mit Hilfe dieser lassen sich Datenbank-Operationen mit Standard Java-Stream Operationen wie z.B. map, filter oder sort nutzen.

Wie funktioniert JPA-Streamer?

JPA-Streamer nutzt Annotation Processing, um für alle mit @Entity gekennzeichneten Klassen ein Meta-Modell zur Compile-Zeit zu erzeugen, das vom Query-Optimizer von JPA-Streamer interpretiert und genutzt werden kann.

Wie sieht das im Code aus?

Unsere Beispiel-Entity Book (getter/setter nicht enthalten):

@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<>();

}

Vom JPA-Streamer bekommt die Klasse eine Book$ zusätzlich:

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
    );
}

Welchen Nutzen hat das?

Diese Book$-Klasse kann nun genutzt werden, um JPA-Queries via Streaming-API zu nutzen. Dies kann helfen, Abfragen schneller und einfacher zu schreiben.

Dazu erstmal ein einfaches Beispiel: Es soll ermittelt werden, wie viele Buchtitel mit „G“ anfangen.

Das SQL dazu könnte wie folgt aussehen:

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

Die Umsetzung mit dem CriteriaBuilder (die Implementierung liefert Hibernate):

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();

Das gleiche Beispiel mit der JPA-Streamer Bibliothek:

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

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

Man beachte, dass im Filter die von JPA-Streamer generierte Klasse verwendet wird, sonst funktioniert das Ganze nicht.

Beim Ausführen wird in beiden Fällen die folgende Query generiert:

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

Das JPA-Streamer Beispiel ist jedoch wesentlich schlanker und deutlich verständlicher.

Ein weiteres, etwas komplexeres Beispiel für Projections (Projections kommen immer dann zum Einsatz, wenn nicht alle Felder benötigt werden): Wir möchten statt der Bücher nur die Buchtitel selektieren, die mit D anfangen. Die Liste soll dabei sortiert werden.

Das SQL dazu:

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

Hier wieder die Umsetzung mit dem 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();

Und die Umsetzung mit 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 beiden Fällen sind die generierten Queries identisch zu unserem SQL-Statement:

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

Wie zuvor auch, liest sich der Code von JPA-Streamer etwas einfacher als das Pendant vom CriteriaBuilder.

Was kann die JPA-Streamer Bibliothek noch?

JPA-Streamer unterstützt eine Vielzahl an Datenbank-Operationen. Ein kurzer Überblick:

SQL Java Stream
FROM stream()
SELECT map(Projection.select())
WHERE filter() (before collecting)
ORDER BY sorted()
OFFSET skip()
LIMIT limit()
COUNT count()
GROUP BY collect(groupingBY())
HAVING filter() (after collecting)
DISTINCT distinct()
SELECT map()
UNION concat(s0, s1).distinct()
JOIN flatmap()

Weitere Informationen zu den einzelnen Operationen finden sich in der offiziellen Dokumentation. Im Github-Projekt finden sich weitere Beispiele.

Lohnt sich der Projekteinsatz?

JPA-Streamer ist eine leistungsstarke Bibliothek, die die Entwicklung von JPA-Anwendungen vereinfachen kann. Die Möglichkeit, Abfragen mithilfe von Java-Stream Standard-Operatoren zu schreiben kann dabei helfen, Abfragen schneller und einfacher zu schreiben. Außerdem sind die Abfragen idR leichter zu lesen und zu verstehen. Die Lesbarkeit und Einfachheit hat aber auch wie im CriteriaBuilder und SQL-Abfragen mit steigender Komplexität ihre Grenzen. Zusätzlich fehlen zum jetzigen Stand noch Funktionen bzw. aktuell werden nicht alle Operationen unterstützt. Der Einsatz im Projekt ist daher immer individuell zu bewerten.

Quellen:

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

https://www.youtube.com/watch?v=7Z4YYaFO6wY

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

https://github.com/speedment/jpa-streamer-demo/

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*