Статьи

Напечатайте безопасные запросы для API собственных запросов JPA

Когда вы используете JPA — иногда — JPQL не справится, и вам придется прибегнуть к нативному SQL. С самого начала ORM, такие как Hibernate , оставляли открытую «черную дверь» для этих случаев и предлагали API, аналогичный Spring JdbcTemplate , Apache DbUtils или jOOQ для простого SQL . Это полезно, поскольку вы можете продолжать использовать ORM в качестве единой точки входа для взаимодействия с базой данных.

Однако написание сложного динамического SQL с использованием конкатенации строк утомительно и подвержено ошибкам, а также открывает возможность для уязвимостей, связанных с инъекцией SQL . Использование API безопасного типа, такого как jOOQ, было бы очень полезно, но вам может быть трудно поддерживать две разные модели соединений, транзакций и сессий в одном приложении только для 10-15 собственных запросов.

Но правда в том, что

Вы используете JOOQ для ваших собственных запросов JPA!

Это правда! Есть несколько способов добиться этого.

Извлечение кортежей (т.е. Object [])

Самый простой способ — не использовать какие-либо из расширенных функций JPA, а просто извлекать кортежи в собственной форме Object[] JPA. Предполагая этот простой служебный метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public static List<Object[]> nativeQuery(
    EntityManager em,
    org.jooq.Query query
) {
 
    // Extract the SQL statement from the jOOQ query:
    Query result = em.createNativeQuery(query.getSQL());
 
    // Extract the bind values from the jOOQ query:
    List<Object> values = query.getBindValues();
    for (int i = 0; i < values.size(); i++) {
        result.setParameter(i + 1, values.get(i));
    }
 
    return result.getResultList();
}

Использование API

Это все, что вам нужно для объединения двух API в их простейшей форме для выполнения «сложных» запросов через EntityManager :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
List<Object[]> books =
nativeQuery(em, DSL.using(configuration)
    .select(
        AUTHOR.FIRST_NAME,
        AUTHOR.LAST_NAME,
        BOOK.TITLE
    )
    .from(AUTHOR)
    .join(BOOK)
        .on(AUTHOR.ID.eq(BOOK.AUTHOR_ID))
    .orderBy(BOOK.ID));
 
books.forEach((Object[] book) ->
    System.out.println(book[0] + " " +
                       book[1] + " wrote " +
                       book[2]));

Согласитесь, в результатах нет большой безопасности типов — поскольку мы получаем только Object[] . Мы с нетерпением ждем будущей Java, которая будет поддерживать типы кортежей (или даже записей), такие как Scala или Ceylon .

Поэтому лучшим решением может быть следующее:

Выборка объектов

Предположим, у вас есть следующие очень простые сущности:

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
@Entity
@Table(name = "book")
public class Book {
 
    @Id
    public int id;
 
    @Column(name = "title")
    public String title;
 
    @ManyToOne
    public Author author;
}
 
@Entity
@Table(name = "author")
public class Author {
 
    @Id
    public int id;
 
    @Column(name = "first_name")
    public String firstName;
 
    @Column(name = "last_name")
    public String lastName;
 
    @OneToMany(mappedBy = "author")
    public Set<Book> books;
}

Предположим, мы добавим дополнительный служебный метод, который также передает ссылку на Class в EntityManager :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public static <E> List<E> nativeQuery(
    EntityManager em,
    org.jooq.Query query,
    Class<E> type
) {
 
    // Extract the SQL statement from the jOOQ query:
    Query result = em.createNativeQuery(
        query.getSQL(), type);
 
    // Extract the bind values from the jOOQ query:
    List<Object> values = query.getBindValues();
    for (int i = 0; i < values.size(); i++) {
        result.setParameter(i + 1, values.get(i));
    }
 
    // There's an unsafe cast here, but we can be sure
    // that we'll get the right type from JPA
    return result.getResultList();
}

Использование API

Теперь это довольно удобно, просто поместите свой запрос jOOQ в этот API и получите от него сущности JPA — лучшее из обоих миров, поскольку вы можете легко добавлять / удалять вложенные коллекции из извлеченных сущностей, как если бы вы извлекали их через JPQL:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
List<Author> authors =
nativeQuery(em,
    DSL.using(configuration)
       .select()
       .from(AUTHOR)
       .orderBy(AUTHOR.ID)
, Author.class); // This is our entity class here
 
authors.forEach(author -> {
    System.out.println(author.firstName + " " +
                       author.lastName + " wrote");
     
    books.forEach(book -> {
        System.out.println("  " + book.title);
 
        // Manipulate the entities here. Your
        // changes will be persistent!
    });
});

Выборка EntityResults

Если вы очень дерзкие и испытываете странные чувства к аннотациям или просто хотите пошутить над коллегами перед тем, как уйти в отпуск, вы также можете прибегнуть к помощи javax.persistence.SqlResultSetMapping JPA. Представьте себе следующее объявление сопоставления:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@SqlResultSetMapping(
    name = "bookmapping",
    entities = {
        @EntityResult(
            entityClass = Book.class,
            fields = {
                @FieldResult(name = "id", column = "b_id"),
                @FieldResult(name = "title", column = "b_title"),
                @FieldResult(name = "author", column = "b_author_id")
            }
        ),
        @EntityResult(
            entityClass = Author.class,
            fields = {
                @FieldResult(name = "id", column = "a_id"),
                @FieldResult(name = "firstName", column = "a_first_name"),
                @FieldResult(name = "lastName", column = "a_last_name")
            }
        )
    }
)

По сути, вышеприведенное объявление отображает столбцы базы данных ( @SqlResultSetMapping -> entities -> @EntityResult -> fields -> @FieldResult -> column ) на объекты и их соответствующие атрибуты. С помощью этого мощного метода вы можете генерировать результаты сущностей из любого вида результата SQL-запроса.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public static <E> List<E> nativeQuery(
    EntityManager em,
    org.jooq.Query query,
    String resultSetMapping
) {
 
    // Extract the SQL statement from the jOOQ query:
    Query result = em.createNativeQuery(
        query.getSQL(), resultSetMapping);
 
    // Extract the bind values from the jOOQ query:
    List<Object> values = query.getBindValues();
    for (int i = 0; i < values.size(); i++) {
        result.setParameter(i + 1, values.get(i));
    }
 
    // This implicit cast is a lie, but let's risk it
    return result.getResultList();
}

Обратите внимание, что в приведенном выше API-интерфейсе используется анти-шаблон , что в данном случае нормально, так как JPA, во-первых, не является API-интерфейсом, безопасным для типов.

Использование API

Теперь, опять же, вы можете передать ваш типобезопасный jOOQ-запрос в EntityManager через вышеуказанный API, передавая имя SqlResultSetMapping следующим образом:

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
List<Object[]> result =
nativeQuery(em,
    DSL.using(configuration
       .select(
           AUTHOR.ID.as("a_id"),
           AUTHOR.FIRST_NAME.as("a_first_name"),
           AUTHOR.LAST_NAME.as("a_last_name"),
           BOOK.ID.as("b_id"),
           BOOK.AUTHOR_ID.as("b_author_id"),
           BOOK.TITLE.as("b_title")
       )
       .from(AUTHOR)
       .join(BOOK).on(BOOK.AUTHOR_ID.eq(AUTHOR.ID))
       .orderBy(BOOK.ID)),
    "bookmapping" // The name of the SqlResultSetMapping
);
 
result.forEach((Object[] entities) -> {
    JPAAuthor author = (JPAAuthor) entities[1];
    JPABook book = (JPABook) entities[0];
 
    System.out.println(author.firstName + " " +
                       author.lastName + " wrote " +
                       book.title);
});

Результатом в этом случае снова является Object[] , но на этот раз Object[] не представляет кортеж с отдельными столбцами, но представляет объекты, как объявлено аннотацией SqlResultSetMapping .

Этот подход интригует и, вероятно, его используют, когда вам нужно отобразить произвольные результаты запросов, но при этом все еще требуются управляемые объекты. Мы можем только порекомендовать интересную серию блогов Торбена Янссена об этих расширенных функциях JPA, если вы хотите узнать больше:

Вывод

Выбор между ORM и SQL (или между Hibernate и jOOQ , в частности) не всегда прост.

  • ORM блестят, когда дело доходит до применения постоянства графов объектов, т.е. когда у вас много сложных CRUD, включая сложные стратегии блокировки и транзакций.
  • SQL лучше всего подходит для выполнения массового SQL, как для операций чтения, так и для записи, при выполнении аналитики и создания отчетов.

Когда вам «везет» (как, например, работа проста), ваше приложение находится только с одной стороны, и вы можете выбирать между ORM и SQL. Когда вам «повезет» (как в — оооо, это интересная проблема), вам придется использовать оба. ( См. Также интересную статью Майка Хэдлоу на эту тему )

Сообщение здесь: Вы можете! Используя собственный API запросов JPA, вы можете запускать сложные запросы, используя всю мощь своей РСУБД, и по-прежнему отображать результаты в объекты JPA. Вы не ограничены использованием JPQL.

Примечание

В то время как в прошлом мы критиковали некоторые аспекты JPA (подробнее см., Как JPA 2.1 стал новым EJB 2.0 ), наша критика была в основном сфокусирована на (ан-) использовании JPA аннотаций. Когда вы используете API безопасного типа, например jOOQ, вы можете предоставить компилятору всю необходимую информацию о типе, чтобы легко создавать результаты. Мы убеждены, что будущая версия JPA будет более активно использовать систему типов Java, что позволит более свободно интегрировать SQL, JPQL и сохранение сущностей.