Статьи

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

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

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

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

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

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

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

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

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:

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 .

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

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

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

@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:

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:

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

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

@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-запроса.

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

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 следующим образом:

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);
});

Результатом в этом случае снова является an  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 и сохранение сущностей.