Статьи

Как перевести SQL GROUP BY и агрегаты в Java 8

Я не мог устоять. Я прочитал этот вопрос Хьюго Пруденте о переполнении стека . И я знал, что должен быть лучший способ, чем то, что предлагает JDK.

Вопрос гласит:

Я ищу лямбду, чтобы уточнить данные уже получены. У меня есть сырой набор результатов, если пользователь не меняет дату, которую я хочу использовать лямбда-код Java, чтобы сгруппировать результаты для того времени. И я новичок в лямбдах с Java.

Лямбда, которую я ищу, работает аналогично этому запросу.

1
2
3
4
5
6
SELECT
    z, w,
    MIN(x), MAX(x), AVG(x),
    MIN(y), MAX(y), AVG(y)
FROM table
GROUP BY z, w;

SQL декларативный. Функционального программирования нет.

Прежде чем мы продолжим это обсуждение, давайте установим очень важный факт. SQL — полностью декларативный язык. Функциональные (или «функциональные иш», чтобы поддерживать приверженцев Haskell в мире) языки программирования, такие как Java 8, не являются декларативными. Хотя выражение алгоритмов преобразования данных с использованием функций гораздо более сжато, чем выражение их с использованием объектов, или, что еще хуже, с помощью императивных инструкций, вы все равно явно выражаете алгоритм.

Когда вы пишете SQL, вы не пишете никакой алгоритм. Вы просто описываете результат, который хотите получить. Оптимизатор движка SQL определит алгоритм для вас — например, основываясь на том факте, что у вас может быть индекс по Z но не по W или по (Z, W) .

Хотя простые примеры, подобные этим, могут быть легко реализованы с помощью Java 8, вы быстро столкнетесь с ограничениями Java, как только вам понадобится создавать более сложные отчеты.

Конечно, как мы уже писали в блоге, оптимум достигается, когда вы комбинируете SQL и функциональное программирование .

Как это можно написать на Java 8?

Есть множество способов сделать это. Суть в том, чтобы понять всех участников такой трансформации. И не важно, считаете ли вы это простым или сложным, подходящим для Java 8 или неадекватным, размышление о различных, менее известных частях нового Stream API, безусловно, стоит того, чтобы его использовать.

Основными участниками здесь являются:

  • Поток : Если вы используете библиотеки JDK 8, то вашим выбором будет новый тип java.util.stream.Stream .
  • Сборщик : JDK предоставляет нам довольно низкоуровневый и, следовательно, очень мощный новый API для агрегирования данных (также известный как «сокращение»). Этот API суммируется с новым типом java.util.stream.Collector , новым типом, о котором мы почти ничего не слышали в блогосфере.

отказ

Часть кода, отображаемого здесь, может не работать в вашей любимой IDE. К сожалению, даже если Java 7 достигнет своего конца, у всех основных IDE (Eclipse, IntelliJ, NetBeans) и даже компилятора javac все еще есть довольно много ошибок, связанных с комбинацией вывода обобщенного типа и лямбда-выражений . Оставайтесь с нами, пока эти ошибки не будут исправлены! И сообщайте о любой обнаруженной вами ошибке. Мы все будем вам благодарны за это!

Поехали!

Давайте рассмотрим наш оператор SQL:

1
2
3
4
5
6
SELECT
    z, w,
    MIN(x), MAX(x), AVG(x),
    MIN(y), MAX(y), AVG(y)
FROM table
GROUP BY z, w;

С точки зрения Stream API, сама таблица является Stream . Давайте просто предположим, что у нас есть «тип таблицы» A как таковой:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
    final int w;
    final int x;
    final int y;
    final int z;
 
    A(int w, int x, int y, int z) {
        this.w = w;
        this.x = x;
        this.y = y;
        this.z = z;
    }
 
    @Override
    public String toString() {
        return "A{" +
                "w=" + w +
                ", x=" + x +
                ", y=" + y +
                ", z=" + z +
                '}';
    }
}

Вы также можете добавить equals() и hashCode() если необходимо.

Теперь мы можем легко составить Stream с помощью Stream.of() и некоторых примеров данных:

1
2
3
4
5
6
7
8
9
Stream<A> stream =
Stream.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5));

Теперь следующим шагом будет GROUP BY z, w . К сожалению, сам Stream API не содержит такого удобного метода. Мы должны прибегнуть к более низкоуровневым операциям, указав более общую Stream.collect() и передав ей Collector который выполняет группировку. К счастью, из вспомогательного класса Collectors уже доступно множество различных группирующих Collectors .

Итак, мы добавим это в наш stream :

1
2
3
4
5
6
7
8
9
Stream.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(...));

jool-логотип-черный Теперь начинается интересная часть. Как нам указать, что мы хотим сгруппировать по Az и Aw ? Нам нужно предоставить этому методу groupingBy функцию, которая может извлекать что-то вроде кортежа SQL из типа A Мы могли бы написать свой собственный кортеж или просто использовать библиотеку jOOλ , созданную и открытую для улучшения наших интеграционных тестов jOOQ .

Тип Tuple2 выглядит примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Tuple2<T1, T2> {
 
    public final T1 v1;
    public final T2 v2;
 
    public T1 v1() {
        return v1;
    }
 
    public T2 v2() {
        return v2;
    }
 
    public Tuple2(T1 v1, T2 v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
}
 
public interface Tuple {
    static <T1, T2> Tuple2<T1, T2> tuple(T1 v1, T2 v2) {
        return new Tuple2<>(v1, v2);
    }
}

У него много других полезных функций, но их будет достаточно для этой статьи.

На заметку

Почему JDK не поставляется со встроенными кортежами, такими как C # или Scala, ускользает от меня.

Функциональное программирование без кортежей похоже на кофе без сахара: горький удар в лицо.

Во всяком случае … вернуться на ходу

Таким образом, мы группируем по кортежу (Az, Aw) , как в SQL

01
02
03
04
05
06
07
08
09
10
11
12
Map<Tuple2<Integer, Integer>, List<A>> map =
Stream.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(
    a -> tuple(a.z, a.w)
));

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

Запустив следующее утверждение:

1
map.entrySet().forEach(System.out::println);

даст:

1
2
3
(1, 1)=[A{w=1, x=1, y=1, z=1}, A{w=1, x=2, y=3, z=1}]
(4, 9)=[A{w=9, x=8, y=6, z=4}, A{w=9, x=9, y=7, z=4}]
(5, 2)=[A{w=2, x=3, y=4, z=5}, A{w=2, x=4, y=4, z=5}, A{w=2, x=5, y=5, z=5}]

Это уже довольно круто! Фактически это ведет себя как стандартная агрегатная функция COLLECT() SQL: 2011 , которая также доступна в Oracle 10g +

Теперь вместо фактического сбора записей A мы предпочитаем агрегировать отдельные значения x и y . JDK предоставляет нам несколько интересных новых типов, например, java.util.IntSummaryStatistics , который снова доступен для удобства из типа Collectors.summarizingInt() через Collectors.summarizingInt() .

На заметку

На мой вкус, эта методика агрегации данных кувалдой немного странная. Библиотеки JDK намеренно оставлены на низком уровне и многословны, возможно, для того, чтобы уменьшить занимаемую библиотечную площадь или предотвратить «ужасные» последствия, когда через 5–10 лет (после выпуска JDK 9 и 10) становится очевидным, что некоторые функции возможно, был добавлен преждевременно .

В то же время, существует IntSummaryStatistics все или ничего», которая слепо агрегирует эти популярные значения агрегации для вашей коллекции:

  • COUNT(*)
  • SUM()
  • MIN()
  • MAX()

и очевидно, что когда у вас есть SUM() и COUNT(*) , у вас также есть AVG() = SUM() / COUNT(*) . Так что это будет путь Java. IntSummaryStatistics .

Если вам интересно, стандарт SQL: 2011 определяет эти агрегатные функции:

AVG, MAX, MIN, SUM, EVERY, ANY, SOME, COUNT, STDDEV_POP, STDDEV_SAMP, VAR_SAMP, VAR_POP, COLLECT, FUSION, INTERSECTION, COVAR_POP, COVAR_SAMP, CORR, REGR_SLOPE, REGR_INTERCEPT, REGR_COUNT, REGR_R2, REGR_AVGX, REGR_AVGY, REGR_SXX, REGR_SYY, REGR_SXY, PERCENTILE_CONT, PERCENTILE_DISC, ARRAY_AGG

И, конечно же, в SQL есть много других агрегатных и оконных функций , зависящих от производителя. Мы в блоге о них всех:

Правда, MIN, MAX, SUM, COUNT, AVG , безусловно, самые популярные. Но было бы лучше, если бы они не были включены в эти типы агрегации по умолчанию, но стали доступны гораздо более сочетаемым образом.

Во всяком случае … вернуться на ходу

Если вы хотите оставаться на низком уровне и использовать в основном JDK API, вы можете использовать следующую технику для реализации агрегирования по двум столбцам:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Map<
    Tuple2<Integer, Integer>,
    Tuple2<IntSummaryStatistics, IntSummaryStatistics>
> map = Stream.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5))
.collect(Collectors.groupingBy(
    a -> tuple(a.z, a.w),
    Collector.of(
 
        // When collecting, we'll aggregate data
        // into two IntSummaryStatistics for x and y
        () -> tuple(new IntSummaryStatistics(),
                    new IntSummaryStatistics()),
 
        // The accumulator will simply take
        // new t = (x, y) values
        (r, t) -> {
            r.v1.accept(t.x);
            r.v2.accept(t.y);
        },
 
        // The combiner will merge two partial
        // aggregations, in case this is executed
        // in parallel
        (r1, r2) -> {
            r1.v1.combine(r2.v1);
            r1.v2.combine(r2.v2);
 
            return r1;
        }
    )
));
 
map.entrySet().forEach(System.out::println);

Выше теперь будет печатать

1
2
3
4
5
6
(1, 1)=(IntSummaryStatistics{count=2, sum=3, min=1, average=1.500000, max=2},
        IntSummaryStatistics{count=2, sum=4, min=1, average=2.000000, max=3})
(4, 9)=(IntSummaryStatistics{count=2, sum=17, min=8, average=8.500000, max=9},
        IntSummaryStatistics{count=2, sum=13, min=6, average=6.500000, max=7})
(5, 2)=(IntSummaryStatistics{count=3, sum=12, min=3, average=4.000000, max=5},
        IntSummaryStatistics{count=3, sum=13, min=4, average=4.333333, max=5})

Но очевидно, что никто не захочет писать так много кода. То же самое можно сделать с помощью jOOλ с гораздо меньшим количеством кода

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Map<
    Tuple2<Integer, Integer>,
    Tuple2<IntSummaryStatistics, IntSummaryStatistics>
> map =
 
// Seq is like a Stream, but sequential only,
// and with more features
Seq.of(
    new A(1, 1, 1, 1),
    new A(1, 2, 3, 1),
    new A(9, 8, 6, 4),
    new A(9, 9, 7, 4),
    new A(2, 3, 4, 5),
    new A(2, 4, 4, 5),
    new A(2, 5, 5, 5))
 
// Seq.groupBy() is just short for
// Stream.collect(Collectors.groupingBy(...))
.groupBy(
    a -> tuple(a.z, a.w),
 
    // ... because once you have tuples,
    // why not add tuple-collectors?
    Tuple.collectors(
        Collectors.summarizingInt(a -> a.x),
        Collectors.summarizingInt(a -> a.y)
    )
));

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

1
2
3
4
5
6
SELECT
    z, w,
    MIN(x), MAX(x), AVG(x),
    MIN(y), MAX(y), AVG(y)
FROM table
GROUP BY z, w;

Интересной частью здесь является тот факт, что у нас есть то, что мы называем «сборщиками кортежей», Collector который собирает данные в кортежи агрегированных результатов для любой степени кортежа (до 8). Вот код для Tuple.collectors :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// All of these generics... sheesh!
static <T, A1, A2, D1, D2>
       Collector<T, Tuple2<A1, A2>, Tuple2<D1, D2>>
collectors(
    Collector<T, A1, D1> collector1
  , Collector<T, A2, D2> collector2
) {
    return Collector.of(
        () -> tuple(
            collector1.supplier().get()
          , collector2.supplier().get()
        ),
        (a, t) -> {
            collector1.accumulator().accept(a.v1, t);
            collector2.accumulator().accept(a.v2, t);
        },
        (a1, a2) -> tuple(
            collector1.combiner().apply(a1.v1, a2.v1)
          , collector2.combiner().apply(a1.v2, a2.v2)
        ),
        a -> tuple(
            collector1.finisher().apply(a.v1)
          , collector2.finisher().apply(a.v2)
        )
    );
}

Где Tuple2<D1, D2> — это тип результата агрегации, который мы извлекаем из collector1 (который предоставляет D1 ) и из collector2 (который обеспечивает D2 ).

Вот и все. Были сделаны!

Вывод

Java 8 — это первый шаг к функциональному программированию на Java. Используя потоки и лямбда-выражения, мы уже можем достичь совсем немного. API-интерфейсы JDK, однако, находятся на крайне низком уровне, и опыт использования таких IDE, как Eclipse, IntelliJ или NetBeans, все еще может быть немного разочаровывающим. При написании этой статьи (и добавлении Tuple.collectors() ) я сообщил о 10 ошибках в различных IDE. Некоторые ошибки компилятора javac еще не исправлены до JDK 1.8.0_40 ea. Другими словами:

Я просто продолжаю бросать параметры универсального типа в чертову штуку, пока компилятор не перестанет на меня гнить

Но мы на правильном пути. Я верю, что более полезный API будет поставляться с JDK 9 и особенно с JDK 10, когда, как мы надеемся, все вышеперечисленное получит выгоду от новых типов значений и специализации универсальных типов .

jool-логотип-черный Мы создали jOOλ, чтобы добавить недостающие фрагменты в библиотеки JDK. Если вы хотите пойти ва-банк в функциональном программировании, то есть, когда ваш словарный запас включает в себя хипстерские термины (не могли устоять), такие как монады, моноиды, функторы и все такое, мы предлагаем вам полностью пропустить потоки JDK и jOOλ и начать скачивать функционалjava Марк Перри или Джавасланг Дэниел Дитрих

Ссылка: Как перевести SQL GROUP BY и агрегаты в Java 8 от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .