Статьи

Создание сводных таблиц с потоками Java из баз данных

Необработанные данные из строк и таблиц базы данных не дают такого большого понимания читателям. Вместо этого люди с большей вероятностью увидят шаблоны данных, если мы выполним какую-то агрегацию данных
прежде чем он будет представлен нам. Сводная таблица — это особая форма агрегации, в которой мы можем применять такие операции, как сортировка, усреднение или суммирование, а также часто группирование значений столбцов.

В этой статье я покажу, как можно вычислять сводные таблицы данных из базы данных на чистой Java без написания одной строки SQL. Вы можете легко использовать и изменять примеры в этой статье в соответствии со своими потребностями.

В приведенных ниже примерах я использовал Speedment с открытым исходным кодом , который представляет собой ORM Java Stream, и контент базы данных фильмов Sakila с открытым исходным кодом для MySQL. Ускорение работает для любых основных типов реляционных баз данных, таких как MySQL, PostgreSQL, Oracle, MariaDB, Microsoft SQL Server, DB2, AS400 и других.

Поворотная

Я Actor объекты « Map Actor и для каждого Actor соответствующий List рейтингов фильмов, в которых появился конкретный Actor . Вот пример того, как сводная запись для конкретного Actor может выглядеть выраженной в устной форме:

«Джон Доу участвовал в 9 фильмах с рейтингом« PG-13 »и 4 фильмах с рейтингом« R ».

Мы собираемся вычислить сводные значения для всех действующих лиц в базе данных. База данных Sakila имеет три таблицы интереса для этого конкретного приложения:

1) «пленка», содержащая все фильмы и способ их оценки (например, «PG-13», «R» и т. Д.).
2) «актеры», содержащие (составленных) актеров (например, «МАЙКЛ БОЛГЕР», «ЛАУРА БРОДИ» и т. Д.).
3) «film_actor», который связывает фильмы и актеров вместе в отношениях «многие ко многим».

Первая часть решения включает в себя объединение этих трех таблиц. Объединения создаются с помощью JoinComponent JoinComponent который можно получить следующим образом:

1
2
3
4
5
// to see how a Speedment app is created. It is easy!
Speedment app = …;
 
JoinComponent joinComponent = app.getOrThrow(JoinComponent.class);

Получив JoinComponent , мы можем начать определять отношения Join, необходимые для вычисления нашей сводной таблицы:

1
2
3
4
5
Join<Tuple3<FilmActor, Film, Actor>> join = joinComponent
        .from(FilmActorManager.IDENTIFIER)
        .innerJoinOn(Film.FILM_ID).equal(FilmActor.FILM_ID)
        .innerJoinOn(Actor.ACTOR_ID).equal(FilmActor.ACTOR_ID)
        .build(Tuples::of);

build() принимает ссылку на метод Tuples::of , которая преобразуется в конструктор, который принимает три объекта типа; FilmActor , Film и Actor и это создаст составной неизменяемый Tuple3 включающий эти конкретные объекты. Кортежи встроены в Speedment.

Вооружившись нашим объектом Join, мы теперь можем создать нашу сводную карту, используя стандартный поток Java, полученный из объекта Join:

01
02
03
04
05
06
07
08
09
10
11
12
Map<Actor, Map<String, Long>> pivot = join.stream()
    .collect(
        groupingBy(
            // Applies Actor as a first classifier
            Tuple3::get2,
            groupingBy(
                // Applies rating as second level classifier
                tu -> tu.get1().getRating().get(),
                counting() // Counts the elements
                )
            )
        );

Теперь, когда вычислена сводная Map , мы можем напечатать ее содержимое следующим образом:

1
2
3
4
5
6
7
8
// pivot keys: Actor, values: Map<String, Long>
pivot.forEach((k, v) -> {
    System.out.format(
        "%22s  %5s %n",
        k.getFirstName() + " " + k.getLastName(),
        V
    );
});

Это даст следующий результат:

1
2
3
4
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}
...

Миссия выполнена! В приведенном выше коде метод Tuple3::get2 будет извлекать третий элемент из кортежа ( Actor ), тогда как метод tu.get1() будет извлекать второй элемент из кортежа ( Film ).

Speedment автоматически отобразит код SQL из Java и преобразует результат в поток Java. Если мы включим потоковое ведение журнала, мы сможем точно увидеть, как был обработан SQL:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
SELECT
    A.`actor_id`,A.`film_id`,A.`last_update`,
    B.`film_id`,B.`title`,B.`description`,
    B.`release_year`,B.`language_id`,B.`original_language_id`,
    B.`rental_duration`,B.`rental_rate`,B.`length`,
    B.`replacement_cost`,B.`rating`,B.`special_features`,
    B.`last_update`, C.`actor_id`,C.`first_name`,
    C.`last_name`,C.`last_update`
FROM
    `sakila`.`film_actor` AS A
INNER JOIN
    `sakila`.`film` AS B ON (B.`film_id` = A.`film_id`)
INNER JOIN
    `sakila`.`actor` AS C ON (C.`actor_id` = A.`actor_id`)

Объединяет пользовательские кортежи

Как мы заметили в приведенном выше примере, у нас нет фактического использования объекта FilmActor в потоке, поскольку он используется только для связывания сущностей Film и Actor во время фазы соединения. Кроме того, универсальный Tuple3 имел общие get0() , get1() и get2() которые ничего не говорили о том, что они содержали.

Все это можно исправить, определив наш собственный «кортеж» под названием ActorRating например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private static class ActorRating {
    private final Actor actor;
    private final String rating;
 
    public ActorRating(FilmActor fa, Film film, Actor actor) {
        // fa is not used. See below why
        this.actor = actor;
        this.rating = film.getRating().get();
    }
 
    public Actor actor() {
        return actor;
    }
 
    public String rating() {
        return rating;
    }
 
}

Когда объекты Join создаются с использованием метода build() , мы можем предоставить собственный конструктор, который мы хотим применить к входящим объектам из базы данных. Это функция, которую мы собираемся использовать, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
Join<ActorRating> join = joinComponent
    .from(FilmActorManager.IDENTIFIER)
    .innerJoinOn(Film.FILM_ID).equal(FilmActor.FILM_ID)
    .innerJoinOn(Actor.ACTOR_ID).equal(FilmActor.ACTOR_ID)
    .build(ActorRating::new); // Use a custom constructor
 
Map<Actor, Map<String, Long>> pivot = join.stream()
    .collect(
        groupingBy(
            ActorRating::actor,
            groupingBy(
                ActorRating::rating,
                counting()
            )
         )
    );

В этом примере мы доказали класс с конструктором (ссылка на метод ActorRating:new получает разрешение для new ActorRating(fa, actor, film) ), который просто отбрасывает связывающий объект FilmActor . Класс также предоставил лучшие имена для своих свойств, что сделало код более читабельным. Решение с пользовательским классом ActorRating даст тот же результат, что и в первом примере, но при использовании выглядит намного лучше. Я думаю, что в большинстве случаев написание собственного кортежа стоит дополнительных усилий по сравнению с использованием универсальных кортежей.

Использование параллельного поворота

Одна из замечательных особенностей Speedment заключается в том, что он поддерживает метод Stream parallel() из коробки. Таким образом, если у вас есть сервер с большим количеством процессоров, вы можете использовать все эти ядра процессоров при выполнении запросов к базе данных и объединений. Вот как будет выглядеть параллельное вращение:

01
02
03
04
05
06
07
08
09
10
11
Map<Actor, Map<String, Long>> pivot = join.stream()
    .parallel()  // Make our Stream parallel
    .collect(
        groupingBy(
            ActorRating::actor,
            groupingBy(
                ActorRating::rating,
                counting()
            )
         )
    );

Нам нужно только добавить одну строку кода, чтобы получить параллельное агрегирование. Стратегия параллельного разделения по умолчанию начинает действовать, когда мы достигаем 1024 элементов. Таким образом, параллельное вращение будет происходить только для таблиц или соединений большего размера. Следует отметить, что база данных Sakila содержит только 1000 фильмов, поэтому нам пришлось бы запускать код в более крупной базе данных, чтобы фактически получить выгоду от параллелизма.

Возьми это за спин!

В этой статье мы показали, как можно вычислить сводные данные из базы данных на Java без написания одной строки кода SQL. Посетите Speedment с открытым исходным кодом на GitHub, чтобы узнать больше.

Подробнее о других функциях читайте в руководстве пользователя .

См. Оригинальную статью здесь: Создание сводных таблиц с помощью потоков Java из баз данных.

Мнения, высказанные участниками Java Code Geeks, являются их собственными.