Статьи

Java 8 пятница: больше не нужно ORM

В Data Geekery мы любим Java. И так как мы действительно входим в свободный API jOOQ и запросы DSL , мы абсолютно взволнованы тем, что Java 8 принесет в нашу экосистему.

Ява 8 Пятница

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

Больше нет необходимости в ОРМ

Последние десять лет ведутся дебаты о полезности ORM (объектно-реляционного отображения) . В то время как многие согласны с тем, что Hibernate и JPA очень хорошо решают многие проблемы (главным образом, постоянство сложных графов объектов), другие могут утверждать, что сложность отображения в основном избыточна для приложений, ориентированных на данные .

JPA решает проблемы с отображением, устанавливая стандартизированные декларативные правила отображения с помощью встроенных аннотаций для целевых типов получения. Мы утверждаем, что многие проблемы, связанные с данными, не должны ограничиваться узкой областью применения этих аннотаций, а должны решаться гораздо более функциональным способом. Java 8 и новый Streams API наконец-то позволяют нам сделать это очень лаконично!

Давайте начнем с простого примера, где мы используем команду INFORMATION_SCHEMA H2, чтобы собрать все таблицы и их столбцы. Мы хотим создать специальную структуру данных типа Map<String, List<String>> будет содержать эту информацию. Для простоты взаимодействия с SQL мы будем использовать jOOQ (как всегда, шокер в этом блоге). Вот как мы готовим это:

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
public static void main(String[] args)
throws Exception {
    Class.forName("org.h2.Driver");
    try (Connection c = getConnection(
            "jdbc:h2:~/sql-goodies-with-mapping",
            "sa", "")) {
 
        // This SQL statement produces all table
        // names and column names in the H2 schema
        String sql =
            "select table_name, column_name " +
            "from information_schema.columns " +
            "order by " +
                "table_catalog, " +
                "table_schema, " +
                "table_name, " +
                "ordinal_position";
 
        // This is jOOQ's way of executing the above
        // statement. Result implements List, which
        // makes subsequent steps much easier
        Result<Record> result =
        DSL.using(c)
           .fetch(sql)
    }
}

Теперь, когда мы настроили этот запрос, давайте посмотрим, как мы можем создать Map<String, List<String>> из результата jOOQ:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
DSL.using(c)
   .fetch(sql)
   .stream()
   .collect(groupingBy(
       r -> r.getValue("TABLE_NAME"),
       mapping(
           r -> r.getValue("COLUMN_NAME"),
           toList()
       )
   ))
   .forEach(
       (table, columns) ->
           System.out.println(table + ": " + columns)
   );

Приведенный выше пример производит следующий вывод:

1
2
3
FUNCTION_COLUMNS: [ALIAS_CATALOG, ALIAS_SCHEMA, ...]
CONSTANTS: [CONSTANT_CATALOG, CONSTANT_SCHEMA, ...]
SEQUENCES: [SEQUENCE_CATALOG, SEQUENCE_SCHEMA, ...]

Как это работает? Давайте пройдемся по шагам

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
DSL.using(c)
   .fetch(sql)
 
// Here, we transform a List into a Stream
   .stream()
 
// We're collecting Stream elements into a new
// collection type
   .collect(
 
// The Collector is a grouping operation, producing
// a Map
            groupingBy(
 
// The grouping operation's group key is defined by
// the jOOQ Record's TABLE_NAME value
       r -> r.getValue("TABLE_NAME"),
 
// The grouping operation's group value is generated
// by this mapping expression...
       mapping(
 
// ... which is essentially mapping each grouped
// jOOQ Record to the Record's COLUMN_NAME value
           r -> r.getValue("COLUMN_NAME"),
 
// ... and then collecting all those values into a
// java.util.List. Whew
           toList()
       )
   ))
 
// Once we have this List<String, List<String>> we
// can simply consume it with the following Consumer
// lambda expression
   .forEach(
       (table, columns) ->
           System.out.println(table + ": " + columns)
   );

Понял? Эти вещи, конечно, немного сложнее, когда играешь с ним в первый раз. Сочетание новых типов, обширных обобщений, лямбда-выражений поначалу может быть немного запутанным. Лучше всего просто практиковаться с этими вещами, пока вы не освоитесь с этим. В конце концов, весь API Streams действительно революция по сравнению с предыдущими API коллекций Java.

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

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

1
import static java.util.stream.Collectors.*;

Также обратите внимание, что вывод больше не был упорядочен как в базе данных. Это потому, что сборщик groupingBy возвращает java.util.HashMap . В нашем случае мы могли бы предпочесть собирать вещи в java.util.LinkedHashMap , который сохраняет порядок вставки / сбора:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
DSL.using(c)
   .fetch(sql)
   .stream()
   .collect(groupingBy(
       r -> r.getValue("TABLE_NAME"),
 
       // Add this Supplier to the groupingBy
       // method call
       LinkedHashMap::new,
       mapping(
           r -> r.getValue("COLUMN_NAME"),
           toList()
       )
   ))
   .forEach(...);

Мы могли бы продолжить с другими средствами преобразования результатов. Давайте представим, что мы хотели бы создать упрощенный DDL из приведенной выше схемы. Это очень просто. Сначала нам нужно выбрать тип данных столбца. Мы просто добавим его в наш SQL-запрос:

01
02
03
04
05
06
07
08
09
10
11
String sql =
    "select " +
        "table_name, " +
        "column_name, " +
        "type_name " + // Add the column type
    "from information_schema.columns " +
    "order by " +
        "table_catalog, " +
        "table_schema, " +
        "table_name, " +
        "ordinal_position";

Я также ввел новый локальный класс для примера, чтобы обернуть атрибуты name и type:

1
2
3
4
5
6
7
8
9
class Column {
    final String name;
    final String type;
 
    Column(String name, String type) {
        this.name = name;
        this.type = type;
    }
}

Теперь давайте посмотрим, как мы изменим наши вызовы метода Streams 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
result
    .stream()
    .collect(groupingBy(
        r -> r.getValue("TABLE_NAME"),
        LinkedHashMap::new,
        mapping(
 
            // We now collect this new wrapper type
            // instead of just the COLUMN_NAME
            r -> new Column(
                r.getValue("COLUMN_NAME", String.class),
                r.getValue("TYPE_NAME", String.class)
            ),
            toList()
        )
    ))
    .forEach(
        (table, columns) -> {
 
            // Just emit a CREATE TABLE statement
            System.out.println(
                "CREATE TABLE " + table + " (");
 
            // Map each "Column" type into a String
            // containing the column specification,
            // and join them using comma and
            // newline. Done!
            System.out.println(
                columns.stream()
                       .map(col -> "  " + col.name +
                                    " " + col.type)
                       .collect(Collectors.joining(",\n"))
            );
 
            System.out.println(");");
        }
    );

Вывод не может быть более потрясающим!

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
CREATE TABLE CATALOGS(
  CATALOG_NAME VARCHAR
);
CREATE TABLE COLLATIONS(
  NAME VARCHAR,
  KEY VARCHAR
);
CREATE TABLE COLUMNS(
  TABLE_CATALOG VARCHAR,
  TABLE_SCHEMA VARCHAR,
  TABLE_NAME VARCHAR,
  COLUMN_NAME VARCHAR,
  ORDINAL_POSITION INTEGER,
  COLUMN_DEFAULT VARCHAR,
  IS_NULLABLE VARCHAR,
  DATA_TYPE INTEGER,
  CHARACTER_MAXIMUM_LENGTH INTEGER,
  CHARACTER_OCTET_LENGTH INTEGER,
  NUMERIC_PRECISION INTEGER,
  NUMERIC_PRECISION_RADIX INTEGER,
  NUMERIC_SCALE INTEGER,
  CHARACTER_SET_NAME VARCHAR,
  COLLATION_NAME VARCHAR,
  TYPE_NAME VARCHAR,
  NULLABLE INTEGER,
  IS_COMPUTED BOOLEAN,
  SELECTIVITY INTEGER,
  CHECK_CONSTRAINT VARCHAR,
  SEQUENCE_NAME VARCHAR,
  REMARKS VARCHAR,
  SOURCE_DATA_TYPE SMALLINT
);

В восторге? Эра ORM, возможно, закончилась только сейчас

Это сильное утверждение. Эра ORM, возможно, закончилась. Почему? Потому что использование функциональных выражений для преобразования наборов данных является одним из самых мощных понятий в разработке программного обеспечения. Функциональное программирование очень выразительно и очень универсально. Он лежит в основе обработки данных и потоков данных. Мы, разработчики Java, уже знаем существующие функциональные языки. Например, каждый раньше использовал SQL. Думаю об этом. С помощью SQL вы объявляете источники таблиц, проецируете / трансформируете их в новые потоки кортежей и передаете их либо как производные таблицы в другие высокоуровневые операторы SQL, либо в свою программу Java.

Если вы используете XML, вы можете объявить преобразование XML с помощью XSLT и передать результаты другим объектам обработки XML, например другой таблице стилей XSL, используя конвейерную обработку XProc .

Потоки Java 8 больше ничего. Использование SQL и Streams API — одна из самых мощных концепций обработки данных. Если вы добавите jOOQ в стек, вы сможете получить безопасный доступ к записям базы данных и API запросов. Представьте себе, что вы пишете предыдущий оператор, используя свободный API jOOQ, вместо использования строк SQL.

jooq-The-лучший способ к записи-SQL-в-Java

Вся цепочка методов может быть одной единой цепочкой преобразования данных, как таковой:

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
41
42
43
44
45
46
DSL.using(c)
   .select(
       COLUMNS.TABLE_NAME,
       COLUMNS.COLUMN_NAME,
       COLUMNS.TYPE_NAME
   )
   .from(COLUMNS)
   .orderBy(
       COLUMNS.TABLE_CATALOG,
       COLUMNS.TABLE_SCHEMA,
       COLUMNS.TABLE_NAME,
       COLUMNS.ORDINAL_POSITION
   )
   .fetch()  // jOOQ ends here
   .stream() // Streams start here
   .collect(groupingBy(
       r -> r.getValue(COLUMNS.TABLE_NAME),
       LinkedHashMap::new,
       mapping(
           r -> new Column(
               r.getValue(COLUMNS.COLUMN_NAME),
               r.getValue(COLUMNS.TYPE_NAME)
           ),
           toList()
       )
   ))
   .forEach(
       (table, columns) -> {
            // Just emit a CREATE TABLE statement
            System.out.println(
                "CREATE TABLE " + table + " (");
 
            // Map each "Column" type into a String
            // containing the column specification,
            // and join them using comma and
            // newline. Done!
            System.out.println(
                columns.stream()
                       .map(col -> "  " + col.name +
                                    " " + col.type)
                       .collect(Collectors.joining(",\n"))
            );
 
           System.out.println(");");
       }
   );

Java 8 — это будущее, и с помощью jOOQ, Java 8 и Streams API вы можете написать мощные API преобразования данных. Надеюсь, мы вас так же взволновали! Оставайтесь с нами для более удивительного контента Java 8 в этом блоге.

Ссылка: Java 8, пятница: больше не нужно ORM от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .