Stream Examples

The section presents several examples that combines the operations described in the previous chapter. All examples are part of the JPAstreamer Demo that can be located here and tested locally on your computer.

The examples use the MySQL example database Sakila. It models a classic movie rental store and contains tables such as Film, Actor, Language etc.

Paging

The following example demonstrates how to serve a request for pages from a GUI or a similar application. The page number (starting with page = 0) and ordering will be given as parameters:

private static final int PAGE_SIZE = 20;

private static List<Film> filmPage(JPAStreamer jpaStreamer, int page, Comparator<Film> comparator) {
    return jpaStreamer.stream(Film.class)
        .sorted(comparator)
        .skip(page * PAGE_SIZE)
        .limit(PAGE_SIZE)
        .collect(Collectors.toList());
    }

This method can e.g. be used to fetch the second page of films sorted by title in descending order:

    filmPage(1, Film$.title.reversed());

Partition By

Partitioning is a special case of grouping in which there are only two different classes: false or true. Java has its own partitioner that can be used to classify database entities. The example below classifies the films in two different categories: short and long films, where a long film is of length greater than 120 minutes.

    Map<Boolean, List<Film>> map = jpaStreamer.stream(Film.class)
        .collect(
            Collectors.partitioningBy(Film$.length.greaterThan(120))
        );

    map.forEach((k, v) -> {
        System.out.format("long is %5s has %d films%n", k, v.size());
    });

This will print:

long is false has 543 films
long is  true has 457 films

Projection

There are several Stream operations that limit the number of rows obtained from the database (e.g. filter() and limit()). Although, to limit the number of columns we retrieve, JPAstreamer uses Projections. Projections can be used either initially, to limit the Stream source, or in combination with a map()-operation. The following sections describe both methods.

Initial Stream configuration

By making a StreamConfiguration, we can initially limit the Stream to include only selected columns.

StreamConfiguration<Film> sc = StreamConfiguration.of(Film.class).selecting(Projection.select(Film$.filmId, Film$.title));

jpaStreamer.stream(sc)
    .sorted(Film$.length.reversed())
    .limit(3)
    .forEach(System.out::println);

The Projection is translated by JpaStreamer to a SELECT statement that is applied to the resulting query. This way we limit the given result set to only include values from column film_id and title.

The output reveals that all Object fields which are not obtained are given a value of null whereas all primitive fields are assigned either a value of 0 or false (for boolean values):

Film{filmId=212, title='DARN FORRESTER', description='null', language=null, actors=[], rentalDuration=null, rentalRate=null, length=null, replacementCost=null, rating='null', specialFeatures='null', lastUpdate=null}
Film{filmId=141, title='CHICAGO NORTH', description='null', language=null, actors=[], rentalDuration=null, rentalRate=null, length=null, replacementCost=null, rating='null', specialFeatures='null', lastUpdate=null}
Film{filmId=182, title='CONTROL ANTHEM', description='null', language=null, actors=[], rentalDuration=null, rentalRate=null, length=null, replacementCost=null, rating='null', specialFeatures='null', lastUpdate=null}

Using a map-operation

The downside of using a StreamConfiguration as described above is that we are unable to apply filtering and sorting operations to columns which are not included in the initial configuration. In SQL, this corresponds to the statements on the form SELECT A WHERE B=3. This can be resolved by providing a Projection to the map()-operation as follows:

List<Tuple> tupleList = jpaStreamer.stream(Film.class) (1)
    .sorted(Film$.length.reversed()) (2)
    .limit(3)
    .map(Projection.select(Film$.filmId, Film$.title)) (3)
    .collect(Collectors.toList());

    System.out.println("tupleList = " + tupleList);
1 The Stream source is not limited in any regard
2 We can apply a sorting operation to the column length even though it is not included in the SELECT clause
3 The mapping results in a Tuple with values for the provided columns
This is yet to be optimized in a future release. As of this version, the construct map(Projection.select() works semantically but will still retrieve all columns from the database. See github/speedment/jpa-streamer/issues/23.

Retrieving a numerical column

In the special case of retrieving only a numerical column from the database, you can obtain an IntStream, a DoubleStream or a LongStream using the operations mapToInt(), mapToDouble(), and mapToLong() as follows:

int[] filmLengths = jpaStreamer.stream(Film.class)
    .mapToInt(Film$.length.asInt())
    .toArray();
This is yet to be optimized in a future release.As of this version, the constructs mapToInt() etc. works semantically, but the resulting query will still retrieve all columns from the database.

Joins

Relationships between tables are handled via the JPA provider.Hence, JPAstreamer assumes that the correct relationships are described by the present @Entity beans.The following examples makes use of the three tables Film, Actor and Language from the Sakila database.The corresponding entities are described below:

@Entity
@Table(name = "film", schema = "sakila")
public class Film {

    @Id
    @Column(name = "film_id", columnDefinition = "smallint(5)")
    private Integer filmId;

    @ManyToOne (1)
    @JoinColumn(name="language_id", nullable = false)
    private Language language;

    @ManyToMany(cascade = { CascadeType.ALL }) (2)
    @JoinTable(
            name = "film_actor",
            joinColumns = { @JoinColumn(name = "film_id") },
            inverseJoinColumns = { @JoinColumn(name = "actor_id") }
    )
    Set<Actor> actors = new HashSet<>();

    //...

}
1 A film can only be spoken in one language (simplified reality)
2 A film can starr many actors
@Entity
@Table(name = "language", schema = "sakila")
public class Language {

    @Id
    @Column(name = "language_id", columnDefinition = "tinyint(5)")
    private Integer languageId;

    @OneToMany(mappedBy = "language") (1)
    private List<Film> films;

    //...

}
1 One language can occur in many films
@Entity
@Table(name = "actor", schema = "sakila")
public class Actor {

    @Id
    @Column(name = "actor_id", columnDefinition = "smallint(5)")
    private Integer actorId;

    @ManyToMany(mappedBy = "actors") (1)
    Set<Film> films = new ArrayList<>();

    //...
}
1 An actor can starr in many films

Specifying Stream Joining

So far, we have talked about the relations for joining in entities in other entities (e.g. how an Actor relates to a Film).

It is also important when and how that relation is applied. This can be specified using a StreamConfiguration as shown hereunder:

import static com.speedment.jpastreamer.streamconfiguration.StreamConfiguration.*;
...
jpaStreamer.stream(of(Film.class).joining(Film$.actors).joining(Film$.language))
    .filter(Film$.length.between(100, 120))
    .forEach(System.out::println);

This will create a stream where the film actors and the film language is (JoinType.LEFT) joined into the query, thereby avoiding potential "select N + 1" performance problems.

More generally, it is possible to specify the join type (JoinType.INNER, JoinType.LEFT or JoinType.RIGHT) using the method:

StreamConfiguration<T> joining(Field<T> field, JoinType joinType);

One-to-Many relations

A One-to-Many relationship is defined as a relationship between two tables where a row from a first table can have multiple matching rows in a second table. For example, many films can be in the same language.

The following example maps the languages to a list of all films that are spoken in that language:

Map<Language, Set<Film>> languageFilmMap = jpaStreamer.stream(of(Language.class).joining(Language$.films))
    .collect(toMap(
        Function.identity(),
        Language::getFilms
         )
    );
In this example, we are joining the Language.films thereby preventing an extra query each time Language::getFilms is invoked.

Many-to-One relations

A Many-to-One relationship is defined as a relationship between two tables where many multiple rows from a first table can match the same single row in a second table. For example, a single language may be used in many films.

The following example maps every film with rating PG-13 to its spoken language:

Map<Film, Language> languageMap = jpaStreamer.stream(of(Film.class).joining(Film$.language)) (1)
    .filter(Film$.rating.equal("PG-13"))
    .collect(
            Collectors.toMap(Function.identity(), (2)
                Film::getLanguage (3)
            )
    );
1 Stream over the Film-table (joining in the field language)
2 The identity function assigns the current Film as the key
3 Enters the spoken language as the value

If printed with some formatting this may yield the following output:

ALTER VICTORY: English
APOLLO TEEN: English
AIRPLANE SIERRA: English
...

Many-to-Many relations

A Many-to-Many relationship is defined as a relationship between two tables where many multiple rows from a first table can match multiple rows in a second table. Often a third table is used to form these relations. For example, an actor may participate in several films and a film usually have several actors.

Given the entities above, we can create a filmography that maps every actor to a list of films that they have starred in:

Map<Actor, List<Film>> filmography = jpaStreamer.stream(of(Actor.class).joining(Actor$.films)) (1)
    .collect(
            Collectors.toMap(Function.identity(), (2)
                Actor::getFilms (3)
            )
    );
1 Stream over the Actor-table (joining the field films)
2 The identity function assigns the current Actor as the key
3 Enters the List of films as the value

If printed with some formatting this may yield the following output:

JENNIFER DAVIS: [GREEDY ROOTS, HANOVER GALAXY, ... ]
ED CHASE: [ARTIST COLDBLOODED, IMAGE PRINCESS, ... ]
NICK WAHLBERG: [JEKYLL FROGMEN, CHISUM BEHAVIOR, ... ]
...

Pivot Data

The following example shows a pivot table of all the actors and the number of films they have participated in for each film rating category (e.g. “PG-13”):

    Map<Actor, Map<String, Long>> pivot = jpaStreamer.stream(of(Actor.class).joining(Actor$.films)) (1)
        .collect(
            groupingBy(Function.identity(), (2)
                Collectors.flatMapping(a -> a.getFilms().stream(), (3)
                    groupingBy(Film::getRating, counting()) (4)
                )
            )
        );

As this is a more advanced example it requires some thinking to understand.

1 Streams the Actor table (joining the field films)
2 Assigns Actor as the key
3 Applies a downstream collector which flatmaps all the films which the actor starr in
4 The films are grouped according to rating, and the number of films of each rating is counted
Collectors.flatMapping() is only present in Java 9 and forward.

This will produce the following (slightly simplified) output:

MICHAEL BOLGER  {PG-13=9, R=3, NC-17=6, PG=4, G=8}
LAURA BRODY  {PG-13=8, R=3, NC-17=6, PG=6, G=3}
CAMERON ZELLWEGER  {PG-13=8, R=2, NC-17=3, PG=15, G=5}
...