В этом руководстве можно получить обзор популярных библиотек баз данных и API в Java. Оно охватывает JDBC, Hibernate, JPA, jOOQ, Spring Data и другие.

[Примечание: Руководство содержит более 6000 слов. Вероятно, вам не захочется читать его на мобильном устройстве. Добавьте его в закладки и вернитесь позже.]

Java и базы данных: введение

Всякий раз, когда вы хотите получить доступ к базе данных с помощью вашего (серверного или настольного) Java-приложения, появляются три вопроса:

  • Вы подходите к своему приложению с точки зрения Java или базы данных? Вы хотите сначала написать классы Java или операторы SQL? Вам нужно интегрироваться с существующей базой данных?

  • Как вы выполняете операторы SQL? От небольших операций CRUD (выбрать, вставить, обновить, удалить) до более сложных запросов отчетов SQL (функции аналитики)?

  • Насколько легко вы можете выполнять объектно-реляционное отображение? Что означает отображение между объектами Java и таблицами и строками базы данных?

Чтобы проиллюстрировать концепцию объектно-реляционного отображения, представьте себе такой класс Java:

public class User {

    private Integer id;
    private String firstName;
    private String lastName;

    // Constructor/Getters/Setters....
}

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

id

first_name

last_name

1

hansi

huber

2

max

mutzke

3

donald

trump

Таблица 1. Пользователи

Как вы теперь сопоставляете свой Java-класс с этой таблицей?

Оказывается, в Java есть разные способы сделать это:

  1. JDBC - выбор низкого уровня.

  2. Более удобные и легкие SQL среды, такие как jOOQ или абстракция JDBC Spring.

  3. Полноценные ORM, такие как Hibernate или любая другая реализация JPA.

Мы рассмотрим все различные варианты в этом руководстве, но очень важно сначала понять основы JDBC.

Почему? Потому что все остальные библиотеки, будь то Spring или Hibernate, построены на этих основах - все они используют JDBC под капотом.

JDBC: низкоуровневый доступ к базе данных

Что такое JDBC?

Самый низкоуровневый способ доступа к базам данных в Java - через JDBC API (Java Database Connectivity).

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

Важная особенность JDBC: вам не нужны сторонние библиотеки, поскольку они входят в состав каждого JDK/JRE. Вам нужен только соответствующий JDBC драйвер для вашей конкретной базы данных.

Рекомендация: если вы хотите получить хороший обзор того, как начать работу с JDBC, где найти драйверы, настроить пулы соединений и информацию о выполнении SQL-запросов, я рекомендую вам сначала прочитать статью Что такое JDBC?, а затем продолжайте эту статью.

JDBC в Java: пример

Представьте, что у вас есть база данных, содержащая таблицу  Users, описанную выше. Вы хотите написать запрос, который выбирает всех пользователей из этой таблицы и преобразует их в список объектов Java List<User>.

Небольшой спойлер: JDBC совсем не помогает вам конвертировать из SQL в объекты Java (или наоборот). Рассмотрим код:

package com.marcobehler;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class JdbcQueries {

    public static void main(String[] args) throws SQLException {
        try (Connection conn = DriverManager
                .getConnection("jdbc:mysql://localhost/test?serverTimezone=UTC",
                        "myUsername", "myPassword")) {

            PreparedStatement selectStatement = conn.prepareStatement("select * from users");
            ResultSet rs = selectStatement.executeQuery();

            List<User> users = new ArrayList<>();

            while (rs.next()) { // пройдёт через все строки
                Integer id = rs.getInt("id");
                String firstName = rs.getString("first_name");
                String lastName = rs.getString("last_name");

                User user = new User(id, firstName, lastName);
                users.add(user);
            }
        }
    }
}

Давайте разберемся с этим.

try (Connection conn = DriverManager
        .getConnection("jdbc:mysql://localhost/test?serverTimezone=UTC",
                "myUsername", "myPassword")) {

Здесь мы открываем соединение с базой данных MySQL. Вы не должны забывать включать свой вызов DriverManager.getConnectionв блок try-with-resources, чтобы ваше соединение автоматически закрылось, как только вы закончите блок кода.

PreparedStatement selectStatement = conn.prepareStatement("select * from users");
ResultSet rs = selectStatement.executeQuery();

Вам необходимо создать и выполнить свой оператор SQL, что вы делаете, создавая и выполняя Java PreparedStatement. (PreparedStatements позволяет вам иметь заполнители ? в ваших операторах SQL, но мы пока проигнорируем это.)

List<User> users = new ArrayList<>();

while (rs.next()) { // пройдёт через все строки
    Integer id = rs.getInt("id");
    String firstName = rs.getString("first_name");
    String lastName = rs.getString("last_name");

    User user = new User(id, firstName, lastName);
    users.add(user);
}

Вам необходимо вручную пройти через ResultSet (т.е. все строки, которые возвращает ваш SQL-запрос), а затем вручную создать пользовательские объекты Java, вызывая правильные геттеры для каждой строки ResultSet с правильными именами столбцов и типами (getString()getInt()).

Кроме того, мы также для простоты опустили две вещи в нашем коде выше:

  • Заполнители в более сложных SQL-запросах (пример: select * from USERS where name = ? and registration_date = ?), используемые если вы хотите защитить себя от SQL-инъекций.

  • Обработка транзакций, которая включает открытие и фиксацию транзакций, а также их откат при возникновении ошибки.

Однако приведенный выше пример достаточно хорошо демонстрирует, почему JDBC считается достаточно низкоуровневым. Потому что для взаимодействия SQL и Java требуется много ручной работы.

Резюме о JDBC

При использовании JDBC вы в основном работаете с «голым железом». У вас под рукой вся мощь и скорость SQL и JDBC, но вам нужно убедиться, что вы каким-то образом самостоятельно конвертируете туда и обратно ваши Java объекты в SQL код.

Вы также должны быть хорошим разработчиком и самостоятельно открывать/закрывать соединения с базой данных.

Вот где и появляются более удобные и легкие фреймворки, о которых мы расскажем в следующем разделе.

ORM фреймворки Java: Hibernate, JPA и другие.

Java разработчикам обычно удобнее писать Java классы, чем писать SQL операторы. Следовательно, многие (новые) проекты написаны с использованием подхода Java-First, что означает, что вы создаете свой класс Java до создания соответствующей таблицы базы данных.

Это, естественно, приводит к вопросу объектно-реляционного отображения: как отобразить только что написанный класс Java в таблицу базы данных (которая еще не создана)? И могли бы вы даже сгенерировать свою базу данных из этих Java-классов? По крайней мере, изначально?

Именно здесь вступают в игру полноценные ORM, такие как Hibernate или любая другая реализация JPA.

Что такое Hibernate?

Hibernate - это развитая библиотека ORM (объектно-реляционного отображения), которая впервые была выпущена в 2001 году (!), А текущая стабильная версия 5.4.X и версия 6.x находятся в разработке.

Несмотря на то, что об этом написано бесчисленное количество книг, вот попытка резюмировать, что Hibernate делает хорошо:

  1. Он позволяет (относительно) легко конвертировать между таблицами базы данных и классами java без необходимости делать что-то особенное, кроме начального отображения.

  2. Это позволяет вам не писать код SQL для (базовых) операций CRUD. Подумайте: создание пользователя, удаление пользователя, обновление пользователя.

  3. Она предлагает несколько механизмов запросов (HQL, Criteria API) поверх SQL для запросов к вашим базам данных. А пока предположим, что это «объектно-ориентированные» версии SQL, хотя это немного бессмысленно без примеров, которые будут приведены позже.

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

create table users (
    id integer not null,
    first_name varchar(255),
    last_name varchar(255),
    primary key (id)
)

У вас также есть следующий соответствующий класс Java.

public class User {

        private Integer id;
        private String firstName;
        private String lastName;
        
        //Getters and setters are omitted for brevity
}

Также представьте, что вы загрузили hibernate-core.jar и добавили его в свой проект. Как теперь сообщить Hibernate, что ваш класс User.java должен быть сопоставлен с таблицей базы данных Users?

Вот где в игру вступают аннотации отображения Hibernate.

Как использовать аннотации отображения Hibernate

Очевидно, что Hibernate не имеет возможности узнать, какие из ваших классов должны быть сопоставлены с таблицами базы данных. Следует ли сопоставить класс User.java с таблицей invoices базы данных или с таблицей users?

Исторически, чтобы Hibernate знал, что он должен делать, вы описывали отображение в файле .xml.

Мы не будем рассматривать xml-отображение в этом руководстве, так как за последние пару лет оно было заменено подходом к отображению на основе аннотаций.

Возможно, вы уже сталкивались с некоторыми из этих аннотаций, например, @Entity@Column или @Table. Давайте рассмотрим, как наш аннотированный класс User.java сверху будет выглядеть с этими аннотациями отображения.

(Совет: не копируйте этот код вслепую)

import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.GeneratedValue;
import javax.persistence.Column;
import javax.persistence.Id;

@Entity
@Table(name="users")
public static class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name="first_name")
    private String firstName;

    @Column(name="last_name")
    private String lastName;

        //Геттеры и сеттеры для краткости опущены
}

В задачи данного руководства не входит слишком подробное рассмотрение каждой аннотации, но вот краткий обзор:

  1. @Entity: маркер для Hibernate: сопоставьте этот класс с таблицей базы данных.

  2. @Table: сообщает Hibernate с какой таблицей базы данных нужно сопоставить класс.

  3. @Column: сообщает Hibernate, какой столбец базы данных сопоставить с полем.

  4. @Id и @GeneratedValue: сообщает Hibernate, что такое первичный ключ таблицы и что он автоматически генерируется базой данных.

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

Как быстро начать руботу с Hibernate (5.x)

После аннотирования ваших классов вам все равно нужно загрузить сам Hibernate. Точкой входа в Hibernate практически для всего является так называемая SessionFactory, которую вам нужно настроить.

Она понимает ваши аннотации отображения и позволяет открывать сеансы. Сеанс - это в основном соединение с базой данных (или, точнее, оболочка для вашего старого доброго JDBC-соединения) с дополнительными возможностями поверх. Вы будете использовать эти сеансы для выполнения SQL / HQL / Criteria запросов.

Но сначала немного кода стандартного кода:

В более новых версиях Hibernate (> 5.x) этот код выглядит немного некрасиво, и библиотеки, такие как Spring, позаботятся об этом стандартном коде за вас. Но если вы хотите начать работу с обычным Hibernate, это то, что вам нужно сделать.

(Совет: не копируйте этот код вслепую)

public static void main(String[] args) {
    // Hibernate specific configuration class
    StandardServiceRegistryBuilder standardRegistry
        = new StandardServiceRegistryBuilder()
            .configure()
            .build();

    // Here we tell Hibernate that we annotated our User class
    MetadataSources sources = new MetadataSources( standardRegistry );
    sources.addAnnotatedClass( User.class );
    Metadata metadata = metadataSources.buildMetadata();

    // This is what we want, a SessionFactory!
    SessionFactory sessionFactory = metadata.buildSessionFactory();
}

(посмотрите этот полный пример, чтобы получить больше кода для копирования и вставки)

Как базовая персистентность работает с Hibernate: пример

Теперь, когда у вас настроено отображение и создан SessionFactory, осталось только получить сеанс (читай: соединение с базой данных) из SessionFactory, а затем, например, сохранить пользователя в базе данных.

В терминах Hibernate / JPA это называется «постоянством», потому что вы сохраняете объекты Java в таблицах базы данных. В конце концов, однако, это очень модный способ сказать: сохранить этот объект Java в базе данных для меня, т.е. сгенерировать несколько операторов SQL для вставки.

Да, правильно, вам больше не нужно писать SQL самостоятельно. Hibernate сделает это за вас.

(Совет: не копируйте этот код вслепую)

Session session = sessionFactory.openSession();
User user = new User();
user.setFirstName("Hans");
user.setLastName("Dampf");
// эта строка сгенерирует и выполнит sql "insert into users" за вас!
session.save( user );

По сравнению с обычным JDBC, больше не нужно возиться с PreparedStatements и параметрами, Hibernate обязательно создаст для вас правильный SQL (при условии, что ваши аннотации сопоставления верны!).

Давайте посмотрим, как будут выглядеть простые SQL-операторы select, update и delete.

(Совет: не копируйте этот код вслепую)

// Hibernate генерирует: "select from users where id = 1"
User user = session.get( User.class, 1 );

// Hibernate генерирует: "update users set...where id=1"
session.update(user);

// Hibernate генерирует: "delete from useres where id=1"
session.delete(user);

Как использовать язык запросов Hibernate (HQL)

До сих пор мы рассматривали только базовую персистентность, такую ??как сохранение или удаление объекта User. Но, конечно, бывают случаи, когда вам нужно больше контроля и нужно писать более сложные операторы SQL. Для этого Hibernate предлагает свой собственный язык запросов, так называемый HQL (Hibernate Query Language).

HQL похож на SQL, но ориентирован на ваши объекты Java и фактически не зависит от базовой базы данных SQL. Теоретически это означает, что одни и те же операторы HQL работают для всех баз данных (MySQL, Oracle, Postgres и т. д.) С недостатком, заключающимся в том, что вы теряете доступ ко всем специфическим функциям базы данных.

Что означает «HQL ориентирован на Java объекты»? Давайте посмотрим на пример:

(Совет: не копируйте этот код вслепую)

List<User> users = session.createQuery("select from User u where u.firstName = 'hans'", User.class).list();

session.createQuery("update User u set u.lastName = :newName where u.lastName = :oldName")
            .executeUpdate();

Оба запроса очень похожи на их эквиваленты SQL, но обратите внимание, что вы не обращаетесь к таблицам или столбцам SQL (first_name) в этих запросах.

Вместо этого вы получаете доступ к свойствам (u.firstName) вашего класса User.java! Затем Hibernate обеспечит преобразование этих операторов HQL в правильные, специфичные для базы данных операторы SQL. А в случае выбора данных автоматически преобразовать возвращенные строки в объекты User.

Для получения подробной информации обо всех возможностях HQL обратитесь к разделу HQL в документации Hibernate.

Как использовать Hibernate Criteria API

При написании операторов HQL вы, по сути, все еще пишете или объединяете простые строки (хотя есть поддержка инструментов из таких IDE, как IntelliJ). Сделать ваши HQL/SQL-операторы динамическими (подумайте: разные предложения where в зависимости от ввода пользователя) - это интересная задача.

Для этого Hibernate предлагает другой язык запросов через Criteria API. По сути, существует две версии API критериев (1 и 2), которые существуют параллельно. Версия 1 устарела и когда-нибудь будет удалена в релизе Hibernate 6.x, но ее гораздо проще использовать, чем версию 2.

Написание запросов с критериями (v2) в Hibernate более сложно для изучения и требует дополнительной настройки проекта. Вам необходимо настроить плагин обработки аннотаций для создания «статической метамодели» ваших аннотированных классов (думаете «Пользователь превыше всего»?). А затем напишите несколько очень сложных запросов со сгенерированными классами.

Давайте посмотрим на наш HQL select запрос выше и на то, как его преобразовать в запрос Criteria API.

(Совет: не копируйте этот код вслепую)

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> criteria = builder.createQuery( User.class );
Root<User> root = criteria.from( User.class );
criteria.select( root );
criteria.where( builder.equal( root.get( User_.firstName ), "hans" ) );
List<User> users = entityManager.createQuery( criteria ).getResultList();

Как видите, вы в основном размениваете удобочитаемость и простоту на безопасность типов и гибкость - например, вы можете добавлять if-elses для создания динамических предложений where на лету.

Но имейте в виду, что в нашем базовом примере уже шесть строк кода: для простого «select * from users where firstName =?».

Какие недостатки у Hibernate?

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

Это несколько неожиданно приводит к двум основным проблемам.

  1. Изрядное количество разработчиков рано или поздно заявляет, что «Hibernate творит случайную магию, которую никто не понимает» или что-то в этом духе. Потому что им не хватает базовых знаний о том, что делает Hibernate.

  2. Некоторые разработчики считают, что вам больше не нужно понимать SQL при использовании Hibernate. Реальность такова, что чем сложнее становится ваше программное обеспечение, тем больше навыков SQL вам потребуется для проверки и оптимизации операторов SQL, которые генерирует Hibernate.

Для решения этих двух проблем у вас есть только один выбор: для эффективного использования Hibernate вам необходимо получить хорошее знание Hibernate и SQL.

Какие есть хорошие учебники или книги по Hibernate?

Отличная книга - Java Persistence with Hibernate. В нем 608 страниц, что уже показывает всю сложность всего этого, но ваши навыки Hibernate значительно выиграют от ее чтения.

Если вам нужна более подробная информация о Hibernate, обязательно посетите сайты Влада Михалчи (Vlad Mihalcea) или Торбена Янссена (Thorben Janssen). Оба являются экспертами по Hibernate и регулярно публикуют потрясающий Hibernate контент.

Если вам нравятся видеокурсы, вы также можете посмотреть скринкасты Hibernate на этом сайте. Они уже не самые новые, но дают вам сверхбыстрый старт во вселенную Hibernate.

Что такое Java Persistence API (JPA)?

До сих пор мы говорили только о простом Hibernate, но как насчет JPA? Как это соотносится с такой библиотекой, как Hibernate?

JPA - это просто спецификация, а не реализация или библиотека. Итак, JPA определяет стандарт, какие функции должна поддерживать библиотека, чтобы быть совместимой с JPA. И есть несколько библиотек, как Hibernate, EclipseLink или TopLink, являющихся реализациями спецификации JPA.

Проще говоря: если ваша библиотека поддерживает (например) сохранение объектов в базе данных определенным образом, поддерживает возможности отображения и запросов (например, criteria API и т. д.) и многое другое, то вы можете назвать ее полностью совместимой с JPA.

Таким образом, вместо написания кода, специфичного для Hibernate или EclipseLink, вы пишете код, специфичный для JPA. А затем просто добавьте библиотеку (например, Hibernate) и файл конфигурации в ваш проект JPA, и вы сможете получить доступ к своей базе данных. На практике это означает, что JPA - это еще одна абстракция поверх Hibernate.

Какие текущие версии JPA?

  • JPA 1.0 - утверждена в 2006 г.

  • JPA 2.0 - утверждена в 2009 г.

  • JPA 2.1 - утверждена в 2013 г.

  • JPA 2.2 - утверждена в 2017 г.

Есть несколько блогов, которые подводят итоги изменений в этих спецификациях для вас, но Влад Михалча и Торбен Янссен делают это лучше всего.

В чем же тогда разница между Hibernate и JPA?

Теоретически JPA позволяет не учитывать, какую библиотеку поставщика персистентности (Hibernate, EclipseLink и т. д.) вы используете в своем проекте.

На практике, поскольку Hibernate, безусловно, является самой популярной реализацией JPA, функции JPA часто являются «сильно вдохновленным подмножеством» функций Hibernate. Например, JPQL - это в основном HQL с меньшим количеством функций. И хотя допустимый запрос JPQL всегда является допустимым запросом HQL, наоборот, это не работает.

Таким образом, поскольку сам процесс спецификации JPA требует времени, а результат - это «всего лишь» общий знаменатель существующих библиотек, предлагаемые им функции являются лишь подмножеством, например, всех функций, которые предлагает Hibernate. В противном случае Hibernate, EclipseLink и TopLink были бы совершенно одинаковыми.

Что мне следует использовать - JPA или Hibernate?

В реальных проектах у вас есть два варианта:

  • Вы либо используете JPA в максимально возможной степени, с добавлением специфических функций Hibernate, отсутствующих в спецификации JPA во время вашей разработки.

  • Или полностью использовать простой Hibernate (мой личный, предпочтительный выбор).

Оба способа хороши, ЕСЛИ вы знаете свой SQL.

Как базовая персистентность работает с JPA

В JPA точкой входа для всего кода базы данных является EntityManagerFactory, а также EntityManager.

Давайте посмотрим на приведенный выше пример, где мы сохраняли пользователей с помощью JDBC и Hibernate API. Только на этот раз мы сохраним пользователей с помощью JPA API.

(Совет: не копируйте этот код вслепую)

EntityManagerFactory factory = Persistence.createEntityManagerFactory( "org.hibernate.tutorial.jpa" );

EntityManager entityManager = factory.createEntityManager();
entityManager.getTransaction().begin();
entityManager.persist( new User( "John Wayne") );
entityManager.persist( new User( "John Snow" ) );
entityManager.getTransaction().commit();
entityManager.close();

За исключением разных названий (persist вместо save, EntityManager вместо Session), это читается точно так же, как и обычная версия Hibernate.

Более того, если вы посмотрите исходный код Hibernate, вы обнаружите следующее:

package org.hibernate;

public interface Session extends SharedSessionContract, EntityManager, HibernateEntityManager, AutoCloseable {
  // methods
}

// and

public interface SessionFactory extends EntityManagerFactory, HibernateEntityManagerFactory, Referenceable, Serializable, java.io.Closeable {
    // methods
}

Подводя итог:

  • Hibernate SessionFactory ЭТО JPA EntityManagerFactory

  • Hibernate Session ЭТО JPA EntityManager

Просто так.

Как использовать язык запросов JPA: JPQL

Как уже упоминалось ранее, JPA имеет собственный язык запросов - JPQL. По сути, это сильно вдохновленное подмножество Hibernate HQL, причем запросы JPQL всегда являются действительными запросами HQL, но не наоборот.

Следовательно, обе версии одного и того же запроса будут выглядеть буквально одинаково:

// HQL
int updatedEntities = session.createQuery(
        "update Person " +
        "set name = :newName " +
        "where name = :oldName" )
.setParameter( "oldName", oldName )
.setParameter( "newName", newName )
.executeUpdate();

// JPQL
int updatedEntities = entityManager.createQuery(
        "update Person p " +
        "set p.name = :newName " +
        "where p.name = :oldName" )
.setParameter( "oldName", oldName )
.setParameter( "newName", newName )
.executeUpdate();

Как использовать Criteria API JPA

По сравнению с HQL и JPQL, Criteria API JPA существенно отличается от собственного Criteria API Hibernate. Мы уже рассмотрели API критериев в разделе «Как использовать Hibernate Criteria API».

Какие еще есть реализации JPA?

Существует больше реализаций JPA, чем только Hibernate. В первую очередь приходят на ум два: EclipseLink (см. Hibernate против Eclipselink ) и (более старый) TopLink.

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

Вам, безусловно, нужно активное сообщество для поддержки дальнейшего развития вашей библиотеки, и Hibernate, вероятно, имеет самое активное сообщество по состоянию на 2020 год.

QueryDSL

Как такая библиотека, как QueryDSL, вписывается в раздел JPA этого руководства? До сих пор мы создавали запросы HQL/JPQL вручную (читай: конкатенация строк) или с помощью довольно сложного Criteria API 2.0.

QueryDSL пытается дать вам лучшее из обоих миров. Более легкое построение запросов, чем с Criteria API, большая безопасность типов и меньшая возня, чем с простыми строками.

Следует отметить, что QueryDSL какое-то время не поддерживался, но, начиная с 2020 года, снова набирает обороты. И что он поддерживает не только JPQ, но и базы данных NoSQL, такие как MongoDB или Lucene.

Давайте посмотрим на пример кода QueryDSL, который запускает SQL-запрос "select * from users where first_name =: name".

(Совет: не копируйте этот код вслепую)

QUser user = QUser.user;
JPAQuery<?> query = new JPAQuery<Void>(entityManager);
List<User> users = query.select(user)
  .from(user)
  .where(user.firstName.eq("Hans"))
  .fetch();

Откуда взялся класс QUser? QueryDSL автоматически создаст его для вас из вашего класса User, аннотированного JPA/Hibernate, во время компиляции - с помощью соответствующего плагина компилятора обработки аннотаций.

Затем вы можете использовать эти сгенерированные классы для выполнения типобезопасных запросов к базе данных. Разве они не читают намного лучше, чем эквивалент JPA Criteria 2.0?

Фреймворки ORM в Java: Резюме

Фреймворки ORM - это зрелые и сложные программы. Главное предостережение - думать, что больше не нужно понимать SQL, когда вы работаете с любой из упомянутых реализаций JPA.

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

Главный вывод

Убедитесь, что вы получили хорошее представление о том, как, например, работает Hibernate И как работает SQL и ваша база данных. Тогда вы будете на правильном пути.

Библиотеки Java SQL: облегченный подход

Все следующие библиотеки имеют более легкий подход, ориентированный на базу данных, по сравнению с подходом ORM, ориентированным на Java.

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

jOOQ

jOOQ - популярная библиотека от Лукаса Эдера (Lukas Eder), и она очень хорошо поддерживается. Если вам интересно, он также ведет очень информативный блог обо всем, что касается SQL, баз данных и Java.

По сути, работа с jOOQ сводится к следующему:

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

  2. Вместо того, чтобы писать операторы SQL String с помощью простого JDBC, вы будете использовать эти сгенерированные классы Java для написания ваших SQL-запросов.

  3. jOOQ легко превратит эти Java-классы и запросы в настоящий SQL, выполнит их в базе данных и отобразит результаты обратно в Java-код.

Итак, представьте, что вы настраиваете генератор кода jOOQ и позволяете ему работать с таблицей Users, которая была представлена ??в начале этого руководства. jOOQ сгенерирует свой собственный класс таблицы USERS, который, например, позволит вам выполнить следующий типобезопасный запрос к базе данных:

(Совет: не копируйте этот код вслепую)

// "select u.first_name, u.last_name, s.id from USERS u inner join SUBSCRIPTIONS s
// on u.id = s.user_id where u.first_name = :name"
Result<Record3<String, String, String>> result =
create.select(USERS.FIRST_NAME, USERS.LAST_NAME, SUBSCRIPTIONS.ID)
      .from(USERS)
      .join(SUBSCRIPTIONS)
      .on(USERS.SUBSCRIPTION_ID.eq(SUBSCRIPTIONS.ID))
      .where(USERS.FIRST_NAME.eq("Hans"))
      .fetch();

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

Это также поможет вам получить доступ ко всем функциям вашей базы данных (зависящим от поставщика) (функции окна Think, сводные таблицы, ретроспективные запросы, OLAP, хранимые процедуры, функции конкретного поставщика и т. д.)

Вы найдете более подробное введение в этом кратком руководстве по jOOQ.

MyBatis

MyBatis - еще один популярный и активно поддерживаемый выбор баз данных. (Если вам интересно, MyBatis был разветвлен из IBATIS 3.0 с аналогичным названием, который сейчас находится в проекте Apache Attic).

MyBatis сфокусирован на концепции SQLSessionFactory (не путать с SessionFactory Hibernate). После создания SessionFactory вы можете выполнять операторы SQL для своей базы данных. Эти операторы SQL либо находятся в файлах XML, либо с их помощью вы аннотируете интерфейсы.

Посмотрим, как выглядит пример аннотации:

(Совет: не копируйте этот код вслепую)

package org.mybatis.example;
public interface UserMapper {
  @Select("SELECT * FROM users WHERE id = #{id}")
  User selectUser(int id);
}

Далее этот интерфейс позволяет вам писать такой код:

(Совет: не копируйте этот код вслепую)

UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUser(1);

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

Кроме того, MyBatis уделяет большое внимание возможностям динамического SQL, которые в основном представляют собой основанный на XML способ создания сложных динамических строк SQL (представьте, if-else-when-otherwise внутри ваших операторов SQL).

Jdbi

Jdbi - это небольшая библиотека поверх JDBC, которая делает доступ к базе данных более удобным и не таким сложным в работе, как с обычным JDBC. Это одна из самых низкоуровневых сред SQL.

Его API бывает двух типов, дающих общее представление о том, как с ней работать.

Во-первых, Fluent API :

(Совет о Fluent API: не копируйте этот код вслепую)

Jdbi jdbi = Jdbi.create("jdbc:h2:mem:test"); // (База данных H2 в памяти)

List<User> users = jdbi.withHandle(handle -> {
    handle.execute("CREATE TABLE user (id INTEGER PRIMARY KEY, name VARCHAR)");

    // Именованные параметры из свойств компонента
    handle.createUpdate("INSERT INTO user(id, name) VALUES (:id, :name)")
            .bindBean(new User(3, "David"))
            .execute();

    // Простое отображение в любой тип
    return handle.createQuery("SELECT * FROM user ORDER BY name")
            .mapToBean(User.class)
            .list();
});

Во-вторых, декларативный API, который вы можете увидеть здесь в действии.

fluent-jdbc

Подобно Jdbi, вы найдете библиотеку fluent-jdbc. Опять же, это удобная оболочка для простого JDBC. Посетите его домашнюю страницу, чтобы увидеть больше примеров.

(Совет об API: не копируйте этот код вслепую)

FluentJdbc fluentJdbc = new FluentJdbcBuilder()
        .connectionProvider(dataSource)
        .build();

Query query = fluentJdbc.query();

			query
        .update("UPDATE CUSTOMER SET NAME = ?, ADDRESS = ?")
        .params("John Doe", "Dallas")
        .run();

List<Customer> customers = query.select("SELECT * FROM CUSTOMER WHERE NAME = ?")
        .params("John Doe")
        .listResult(customerMapper);

SimpleFlatMapper

SimpleFlatMapper немного слабоват с точки зрения документации, но это отличная небольшая библиотека, которая помогает вам отображать, скажем, JDBC ResultSets или записи jOOQ на ваши POJO. Фактически, поскольку это «просто» средство отображения, оно интегрируется с большинством фреймворков баз данных, упомянутых в этом руководстве, от Jdbc, jOOQ, queryDSL, JDBI до Spring Jdbc.

Давайте посмотрим на пример интеграции JDBC:

// отобразит набор результатов в POJO User
JdbcMapper<DbObject> userMapper =
    JdbcMapperFactory
        .newInstance()
        .newMapper(User.class)


try (PreparedStatement ps = con.prepareStatement("select * from USERS")) {
    ResultSet rs = ps.executeQuery());
    userMapper.forEach(rs, System.out::println);  //распечатывает все User POJO  
}

Spring JDBC и данные Spring

Spring фреймвок на самом деле огромная экосистема, поэтому вы не должны начинаться сразу с Spring Data (из-за которых вы, вероятно, здесь), а попытаться понять что имееется в ядре Spring.

Шаблон Spring JDBC

Один из старейших вспомогательных классов в Spring (точнее, в зависимости spring-jdbc) называется JDBCTemplate. Он существует с 2001 года, и его не следует путать с Spring Data JDBC.

По сути, это удобная обертка для JDBC соединений, предлагающая лучшую обработку ResultSet, обработку соединений, обработку ошибок, а также интеграцию с фреймвоком Spring Transactional.

Он поставляется в двух вариантах: JdbcTemplate и его (оборачивающий) кузен, NamedParameterJdbcTemplate. Давайте посмотрим на несколько примеров кода, чтобы понять, как бы вы работали со своей базой данных с помощью обоих.

(Совет: не копируйте этот код вслепую)

// plain JDBC template with ? parameters

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

jdbcTemplate.execute("CREATE TABLE users(" +
            "id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))"); (1)

jdbcTemplate.batchUpdate("INSERT INTO users(first_name, last_name) VALUES (?,?)", Arrays.asList("john", "wayne"));  (2)

jdbcTemplate.query(
            "SELECT id, first_name, last_name FROM users WHERE first_name = ?", new Object[] { "Josh" },
            (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name"))
    ).forEach(user -> log.info(user.toString()));   (3)

// именованный шаблон JDBC с именованными параметрами

NamedParameterJdbcTemplate namedTemplate = new NamedParameterJdbcTemplate(datasource);

SqlParameterSource namedParameters = new MapSqlParameterSource().addValue("id", 1);
namedParameterJdbcTemplate.queryForObject(           (4)
  "SELECT * FROM USERS WHERE ID = :id", namedParameters, String.class);
  1. Здесь мы выполняем оператор создания таблицы. Обратите внимание, что по сравнению с обычным JDBC вам больше не нужно перехватывать SQLExceptions, поскольку Spring преобразует их в исключения времени выполнения за вас.

  2. Здесь мы используем замену параметра "?" в JDBC.

  3. Здесь мы используем RowMappers для преобразования простого набора результатов JDBC ResultSet в объекты POJO пользователя из нашего проекта.

  4. NamedParameterJdbcTemplate позволяет ссылаться на параметры в строке SQL по имени (например: id), а не по вопросительному знаку (?).

Глядя на этот код, вы можете заметить, что JDBC шаблон действительно просто отличная обертка вокруг JDBC API, который показывает свои сильные стороны, особенно в проектах Spring (например, где вы можете использовать аннотацию @Transactional).

Как работает Spring Transaction Management: @Transactional

Возможность объявлять транзакции базы данных с помощью аннотации @Transactional - одна из основных сильных сторон Spring.

Аннотации не только избавляют вас от хлопот самостоятельно открывать, обрабатывать и закрывать соединения с базой данных, но Spring также прекрасно интегрируется с внешними библиотеками, такими как Hibernate, jOOQ или любой другой реализацией JPA, обрабатывая для них транзакции.

Давайте еще раз посмотрим на наш предыдущий пример JPA, где вы вручную открываете и фиксируете транзакцию через EntityManager (помните: EntityManager - это на самом деле просто сеанс Hibernate, который представляет собой соединение JDBC на стероидах).

(Совет: не копируйте этот код вслепую)

EntityManagerFactory factory = Persistence.createEntityManagerFactory( "org.hibernate.tutorial.jpa" );

EntityManager entityManager = factory.createEntityManager();
entityManager.getTransaction().begin();
entityManager.persist( new Event( "Our very first event!", new Date() ) );
entityManager.persist( new Event( "A follow up event", new Date() ) );
entityManager.getTransaction().commit();
entityManager.close();

После интеграции Spring и Hibernate / JPA, таким образом «спригифицируя» наш EntityManager, код преобразуется в следующий:

(Совет: не копируйте этот код вслепую)

@PersistenceContext
private EntityManager entityManager;

@Transactional
public void doSomeBusinessLogic() {
    entityManager.persist( new Event( "Наше самое первое мероприятие!", new Date() ) );
    entityManager.persist( new Event( "Последующее мероприятие", new Date() ) );
}

Читается намного лучше, не так ли? Очевидно, что обработка транзакций - это гораздо больше, но это все, что можно быть описать в данном руководстве.

Если вы действительно заинтересованы в понимании транзакций и на самом деле запачкали руки, возможно, вам стоит взглянуть на электронную книгу  Java Database Connections & Transactions. В нем вы найдете массу примеров кода и упражнений для отработки правильной обработки транзакций.

Spring Data JPA

Наконец, пришло время взглянуть на Spring Data, у которого якобы есть «миссия - предоставить основанную на Spring модель программирования для доступа к данным, сохраняя при этом особые черты». Ох, что за общее заявление о миссии из списка Fortune 500. Но что это на самом деле означает?

Само по себе Spring Data - это просто общее название для множества подпроектов:

  1. Два популярных, которые мы собираемся рассмотреть в этом руководстве: Spring Data JDBC и Spring Data JPA.

  2. Многие другие, например, Spring Data REST или Spring Data Redis, или даже Spring Data LDAP. Посетите веб-сайт для получения полного списка.

Что же такое Spring Data JDBC или Spring Data JPA?

По сути, все проекты Spring Data упрощают написание репозиториев или DAO и SQL-запросов. (Есть еще кое-что, но, в данном руководстве, давайте ограничимся этим.)

Быстрое напоминание: общий шаблон в серверных приложениях - создать репозиторий/DAO класс для каждого объекта домена.

Если бы у вас был класс User.java, у вас также будет UserRepository. Тогда этот UserRepository будет иметь такие методы, как findByEmail, findById и т. д. Короче говоря, он позволяет вам выполнять все операции SQL в отношении вашей таблицы Users.

User user = userRepository.findByEmail("my@email.com")

Что интересно в Spring Data - это то, что она понимает аннотации отображения JPA вашего класса User (помните, @Entity, @Column, @Table и т. д.) и автоматически генерирует репозитории для вас!

Это означает, что вы получаете все основные операции CRUD (сохранение, удаление, findBy) бесплатно, без необходимости писать дополнительный код.

Как написать собственные репозитории Spring Data JPA

Давайте посмотрим на несколько примеров кода. Предполагая, что у вас есть соответствующие spring-data- {jdbc | jpa} .jars в вашем пути к классам, с добавлением небольшой конфигурации, вы могли бы написать такой код:

(Совет: не копируйте этот код вслепую)

import org.springframework.data.jpa.repository.JpaRepository;
import com.marcobehler.domain.User;

public interface MyUserRepository extends JpaRepository<User, Long> {

// JpaRepository содержит все эти методы, вам не нужно писать их самостоятельно!

        List<T> findAll();

        List<T> findAll(Sort sort);

        List<T> findAllById(Iterable<ID> ids);

        <S extends T> List<S> saveAll(Iterable<S> entities);

// и многое другое ... что вы можете выполнить без реализации, потому что Spring Data JPA 
// будет автоматически генерировать для вас реализацию - во время выполнения
}

Ваш собственный репозиторий просто должен расширять JPARepository Spring Data (если вы используете JPA), и вы получите методы find * / save * (и другие) бесплатно, поскольку они являются частью интерфейса JPARepository - я просто добавил их в приведенном выше коде для ясности.

Как писать собственные запросы Spring Data JPA / JDBC

Более того, вы можете писать собственные JPA запросы (или пользовательские SQL запросы) по соглашению. Это очень похоже, например, на написание запросов в Ruby on Rails. Как?

import org.springframework.data.jpa.repository.JpaRepository;
import com.marcobehler.domain.User;

public interface MyUserRepository extends JpaRepository<User, Long> {

  List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

Spring при запуске приложения считывает имя метода и транслирует его в соответствующий JPA запрос всякий раз, когда вы выполняете этот метод. (результирующий SQL: "select * from Users where email_address = :emailAddress and lastName = :lastName").

Это немного отличается от Spring Data JDBC (необходимость расширения из другого интерфейса CrudRepository). Spring Data JDBC не поддерживает написание методов запроса. Вместо этого вам придется написать запрос самостоятельно, который будет выполняться напрямую как запрос JDBC, а не как запрос JPA.

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.jdbc.repository.Query;
import com.marcobehler.domain.User;

public interface MyUserRepository extends CrudRepository<User, Long> {

  @Query("select * from Users where email = :emailAddress and lastName = :lastName ")
  List<User> findByEmailAddressAndLastname(@Param("emailAddress") String emailAddress, @Param("lastName") String lastname);
}

Вы можете сделать то же самое с Spring Data JPA (обратите внимание на другой импорт Query аннотации и то, что мы не используем здесь именованные параметры для изменения). Вам все равно не понадобится реализация этого интерфейса.

import org.springframework.data.jpa.repository.Query;

public interface MyUserRepository extends JpaRepository<User, Long> {
    @Query("select u from User u where u.emailAddress = ?1")
    User findByEmailAddress(String emailAddress);
}

Spring Data: сводка

Из сказанного можно сделать несколько выводов:

  1. По сути Spring Data - это простой доступ к данным и, следовательно, репозитории и запросы. Он понимает аннотации отображения javax.persistence и на основе этих аннотаций генерирует для вас DAO.

  2. Spring Data JPA - это удобная библиотека поверх JPA / Hibernate. Дело не в том, что обе библиотеки разные, а в том, что они интегрируются. Он позволяет вам писать супер-простые репозитории JPA, имея при этом доступ ко всему набору функций вашего ORM.

  3. Spring Data JDBC - это удобная библиотека поверх JDBC. Он позволяет вам писать репозитории на основе JDBC без необходимости в полномасштабном ORM и его функциях (кэширование, отложенная загрузка...). Это означает больше контроля и меньше магии за кулисами.

И, что самое главное, Spring Data прекрасно интегрируется с любым другим проектом Spring и это очевидный выбор всякий раз, когда вы создаете проекты Spring Boot.

Опять же, это руководство может дать вам только краткий обзор того, что такое Spring Data, для получения более подробной информации ознакомьтесь с официальной документацией.

Выбор правильной библиотеки

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

  • Независимо от того, какую библиотеку базы данных вы в конечном итоге выберете, убедитесь, что вы хорошо разбираетесь в SQL и базах данных (чего у разработчиков Java обычно нет).

  • Выберите библиотеку с активным сообществом, хорошей документацией и регулярными выпусками.

  • Изучите возможности своей библиотеки баз данных, т.е. потратьте время на чтение этих 608 страниц JPA.

  • Ваш проект можно реализовать как с обычным Hibernate, так и с Hibernate, обернутым в JPA.

  • Можно также будет использовать JooQ или любую из других упомянутых библиотек, ориентированных на базу данных.

  • Вы также можете комбинировать эти библиотеки, например, реализацию JPA и JoOQ или простой JDBC, или добавить больше возможностей с помощью таких библиотек, как QueryDSL.

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

Спасибо за прочтение

Благодарности

Большое спасибо следующим читателям за их подробные отзывы и правки к данному руководству: parmslukasederStew Ashton.