Будущих студентов курса "Java Developer. Professional" и всех интересующихся приглашаем принять участие в открытом уроке на тему "Введение в Spring Data Jdbc".
А сейчас делимся традиционным переводом полезного материала.
В этой статье дается обзор популярных библиотек и API для работы с базами данных в Java, в том числе JDBC, Hibernate, JPA, jOOQ, Spring Data и других.
Java и базы данных: введение
Каждый раз при необходимости взаимодействия с базами данных появляются три вопроса:
Какой подход использовать при разработке: java-first или database-first? Писать сначала Java-классы или SQL-запросы? Будет ли использоваться уже существующая база данных?
Каким способом выполнять SQL-запросы: как простые для CRUD-операций (select from, insert into, update where), так и более сложные для отчетов?
Как проще реализовать объектно-реляционное отображение (object-relational mapping, ORM)? И что вообще значит отображение между объектами Java и таблицами/строками базы данных?
Для иллюстрации концепции объектно-реляционного отображения рассмотрим следующий класс:
public class User {
private Integer id;
private String firstName;
private String lastName;
// Constructor/Getters/Setters....
}
В дополнение к этому классу в базе данных есть таблица USERS, которая может выглядеть следующим образом:
id | first_name | last_name |
1 | hansi | huber |
2 | max | mutzke |
3 | donald | trump |
Как вы будете отображать Java-класс на эту таблицу?
Для этого есть несколько вариантов:
JDBC — самый низкий уровень.
Удобные и легковесные SQL-фреймворки, такие как jOOQ или Spring JDBC.
Полноценные ORM, такие как Hibernate или другие реализации JPA.
В этом руководстве мы рассмотрим различные варианты, но для начала очень важно понять основы JDBC. Зачем? Потому что все библиотеки и фреймворки, будь то Spring или Hibernate, под капотом используют JDBC.
JDBC: низкоуровневый доступ к базе данных
Что такое JDBC?
Самый низкоуровневый способ доступа к базам данных в Java — это использование JDBC API (Java Database Connectivity). Все фреймворки, рассматриваемые ниже, используют JDBC под капотом. И, конечно, вы можете выполнять SQL-запросы, используя JDBC напрямую.
Преимущество JDBC в том, что вам не нужны сторонние зависимости, так как JDBC входит в состав любого JDK / JRE. Вам нужен только соответствующий JDBC-драйвер вашей базы данных.
Если вы хотите узнать больше о том, как начать работу с JDBC, где найти драйверы, как настроить пулы соединений и выполнять SQL-запросы, я рекомендую сначала прочитать мою статью What is JDBC? ("Что такое JDBC?") и после нее вернуться к этой статье.
Пример JDBC
Например, у вас есть база данных с таблицей Users, приведенной выше, вы хотите написать запрос, выбирающий всех пользователей из этой таблицы, и получить результат в виде List<User>
— списка объектов Java.
Небольшой спойлер: 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()) { // will traverse through all rows
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-запрос выполняется через создание и выполнение PreparedStatement
. (PreparedStatement
позволяет использовать плейсхолдеры параметров в виде ?
в запросах, но пока мы это опустим.)
List<User> users = new ArrayList<>();
while (rs.next()) { // will traverse through all rows
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, но вам нужно вручную конвертировать результаты SQL-запросов в объекты Java. Вам также нужно следить за соединениями с базой данных и вручную открывать и закрывать их.
Здесь на сцену выходят удобные и легкие фреймворки, о которых мы расскажем в следующем разделе.
ORM-фреймворки: Hibernate, JPA и другие
Java-разработчикам, как правило, более комфортно писать код на Java, чем на SQL. Поэтому многие новые проекты пишутся с использованием подхода java-first, который означает, что сначала вы создаете Java-классы, а потом соответствующие таблицы базы данных.
Это, естественно, приводит к вопросу объектно-реляционного отображения: как смапить только что написанный Java-класс с таблицей базы данных (которая еще не создана)? И можно ли на основе Java-классов сгенерировать схему базы данных (по крайней мере, первоначальную).
Именно здесь вступают в игру полноценные ORM, такие как Hibernate и другие реализации JPA.
Что такое Hibernate?
Hibernate — это зрелый ORM-фреймворк (Object-Relational Mapping, объектно-реляционное отображение), который впервые был выпущен в 2001 году (!). Текущая стабильная версия 5.4.X, версия 6.x находится в разработке.
Несмотря на то что про Hibernate написано бесчисленное количество книг, я предприму попытку резюмировать его сильные стороны:
Позволяет (относительно) легко преобразовывать таблицы базы данных в java-классы без какого-либо сложного кода, кроме конфигурирования маппинга.
Позволяет не писать SQL-код для таких простых CRUD-операций, как создание, удаление или изменение пользователя.
Предлагает несколько вариантов (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 (пользователи)?
Изначально для настройки маппинга использовались 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;
//Getters and setters are omitted for brevity
}
В рамках данной статьи я не буду подробно разбирать каждую из аннотаций, приведу только краткое описание:
@Entity — маркер для Hibernate, что надо связать этот класс с таблицей базы данных.
@Table — указывает Hibernate, в какую таблицу базы данных нужно отобразить класс.
@Column — указывает Hibernate, в какой столбец базы данных нужно отобразить поле.
@Id и @GeneratedValue — указывает Hibernate, что это первичный ключ таблицы и что он генерируется автоматически базой данных.
Конечно, аннотаций гораздо больше, но вы уже примерно поняли подход с аннотациями. С Hibernate вы пишете свои классы, а затем аннотируете их соответствующими аннотациями.
Как начать работать с Hibernate (5.x)
После аннотирования классов вам нужно настроить сам Hibernate. Точкой входа, практически для всего в Hibernate, является класс SessionFactory
, который вам нужно сконфигурировать.
SessionFactory
обрабатывает ваши аннотации и позволяет создать Session
. Объект Session, по сути, является соединением с базой данных (а точнее, оберткой для старого доброго JDBC-соединения) с дополнительным функционалом поверх. Session используется для выполнения SQL / HQL / Criteria — запросов.
Но сначала немного кода для первоначальной настройки Hibernate.
В последних версиях 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
, осталось только получить Session
(считайте, что соединение с базой данных) из SessionFactory
, а затем, например, сохранить пользователя в базу данных.
В терминах Hibernate / JPA это называется "persistence" (персистентность, постоянство), потому что вы сохраняете объекты Java в таблицы базы данных. Однако, в конце концов, это удобный способ сказать: сохрани этот объект в базе данных, т.е. сгенерируй SQL для INSERT (вставки).
Да, вам больше не нужно писать SQL самому: Hibernate сделает это за вас.
Session session = sessionFactory.openSession();
User user = new User();
user.setFirstName("Hans");
user.setLastName("Dampf");
// this line will generate and execute the "insert into users" sql for you!
session.save( user );
По сравнению с голым JDBC больше не нужно возиться с PreparedStatement
и параметрами, Hibernate позаботится о том, чтобы создать правильный SQL (если ваши аннотации маппинга поставлены правильно!).
Давайте посмотрим, как будут выглядеть простые SQL-выражения (select
, update
и delete
).
// Hibernate generates: "select from users where id = 1"
User user = session.get( User.class, 1 );
// Hibernate generates: "update users set...where id=1"
session.update(user);
// Hibernate generates: "delete from useres where id=1"
session.delete(user);
Как использовать Hibernate Query Language (HQL)
До сих пор мы рассматривали только простые примеры персистентности, такие как сохранение или удаление объекта User
. Но бывают случаи, когда вам нужно больше контроля и более сложные SQL-запросы. Для этого Hibernate предлагает свой язык запросов, так называемый HQL (Hibernate Query Language).
HQL похож на SQL, но ориентирован на Java-объекты и фактически не зависит от используемой СУБД. Теоретически это означает, что один и тот же 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-эквиваленты, но обратите внимание, что вы обращаетесь не к таблицам и столбцам базы данных (first_name
), а к свойствам (u.firstName
) вашего класса User.java
! Затем Hibernate конвертирует этот HQL в соответствующий SQL для конкретной базы данных. В случае с SELECT он автоматически преобразует полученные данные в объекты User.
Для получения подробной информации обо всех возможностях HQL обратитесь к разделу HQL в документации Hibernate.
Как использовать Criteria API
В HQL-запросах вы, по сути, все еще пишете и конкатенируете обычные строки (хотя есть поддержка в IDE, таком как IntelliJ). Но самое интересное — это динамический HQL / SQL (формирование разных WHERE в зависимости от пользовательского ввода).
Для этого Hibernate предлагает другой способ написания запросов — Criteria API. Есть две версии Criteria API (1 и 2), которые существуют одновременно. Версия 1 устарела и когда-нибудь будет удалена в Hibernate 6.x, но она гораздо проще, чем версия 2.
Criteria API (v2) имеет более крутую кривую обучения и требует некоторой дополнительной настройки проекта. Вам необходимо настроить плагин обработки аннотаций для генерации "статической метамодели" ваших аннотированных классов. А затем писать запросы с использованием сгенерированных классов.
Давайте посмотрим на то, как переписать наш пример с HQL на 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-else
для построения динамических выражений where
на лету.
Но обратите внимание, что для нашего простого примера с "select * from users where firstName =?
" теперь получилось целых шесть строк кода.
Недостатки Hibernate
С использованием Hibernate можно реализовать не только простые маппинги, которые были рассмотрены выше. В реальной жизни все бывает намного сложнее. Также Hibernate предлагает массу других удобных функций: каскадные операции, ленивая загрузка (lazy load), кеширование и многое другое. Это действительно сложный фреймворк, который нельзя изучить просто взяв какой-то кусок кода из туториала в свой проект.
Это несколько неожиданно приводит к двум основным проблемам:
Довольно много разработчиков рано или поздно начинают говорить, что "Hibernate творит какую-то магию, которую никто не понимает" или что-то вроде этого. Но это только по причине того, что им не хватает базовых знаний о том, как работает Hibernate.
Некоторые разработчики считают, что при использовании Hibernate больше не нужно разбираться в SQL. Хотя реальность такова, что чем сложнее проект, тем больше знаний и навыков работы с SQL требуется для анализа сгенерированного Hibernate'ом SQL-кода и его оптимизации.
Для решения этих двух проблем у вас есть только один выход: необходимо хорошо изучить Hibernate и SQL.
Какие есть хорошие туториалы и книги по Hibernate?
Отличная книга "Java Persistence with Hibernate". В ней 608 страниц, что уже говорит о сложности Hibernate. Но после ее прочтения вы значительно улучшите свои знания в области Hibernate.
Если вам нужна дополнительная информация, обязательно посетите сайты Влада Михалчи (Vlad Mihalcea) и Торбена Янссена (Thorben Janssen). Они оба являются признанными экспертами по Hibernate и регулярно публикуют потрясающий контент.
Если вы любите смотреть видеокурсы, то можете посмотреть скринкасты по Hibernate на этом сайте. Они уже не самые новые, но дадут вам быстрый старт во вселенную Hibernate.
Что такое Java Persistence API (JPA)?
До сих пор мы говорили только о простом Hibernate, но как насчет JPA? Как он связан с Hibernate?
JPA — это всего лишь спецификация, а не реализация или библиотека. JPA определяет стандарт того, какой функционал должен присутствовать в библиотеке, чтобы она была совместимой с JPA. Есть несколько реализация JPA, например, Hibernate, EclipseLink или TopLink.
Проще говоря, если ваша библиотека поддерживает, например, сохранение объектов в базе данных, предоставляет возможности маппинга и выполняет запросы (например, Criteria API и т. п.) и др., то вы можете назвать ее JPA-совместимой.
Таким образом, вместо написания кода, специфичного для Hibernate или EclipseLink, вы пишете JPA-специфичный код. А затем просто добавляете в JPA-проект библиотеки (Hibernate) с файлом конфигурации, и получаете доступ к базе данных. На практике это означает, что JPA — еще одна абстракция поверх Hibernate.
Текущие версии JPA
JPA 1.0 — утверждена в 2006 г.
JPA 2.0 — утверждена в 2009 г.
JPA 2.1 — утверждена в 2013 г.
JPA 2.2 — утверждена в 2017 г.
Есть множество блогов, которые кратко описывают изменения по версиям, но Vlad Mihalcea и Thorben Janssen делают это лучше всех.
В чем же тогда разница между Hibernate и JPA?
Теоретически JPA позволяет вам не обращать внимания на то, какой persistance-провайдер (Hibernate, EclipseLink и т.д.) вы используете в своем проекте.
Поскольку на практике на сегодняшний день самой популярной реализацией JPA является Hibernate, функционал в JPA часто является подмножеством функционала Hibernate. Например, JPQL — это HQL с меньшим количеством функций. И хотя допустимый запрос JPQL всегда будет допустимым запросом HQL, в обратную сторону это не так.
Таким образом, поскольку сам процесс выпуска спецификации JPA требует времени, а результат — это общий знаменатель функционала существующих библиотек, то предлагаемые им функции являются лишь подмножеством функционала, предлагаемого, например, Hibernate. Иначе Hibernate, EclipseLink и TopLink были бы совершенно идентичными.
Что использовать: JPA или Hibernate?
В реальных проектах у вас, по сути, есть два варианта:
Вы либо используете по максимуму JPA без использования Hibernate-специфичных вещей, отсутствующих в JPA.
Либо везде используете только Hibernate (я предпочитаю этот вариант).
Простой пример работы с 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
Все просто.
Как использовать JPQL
Как уже упоминалось ранее, в JPA есть свой язык запросов — JPQL. По сути, он представляет собой урезанный HQL (Hibernate), при этом запросы 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. Мы уже рассмотрели Criteria API для Hibernate выше в соответствующем разделе.
Какие еще есть реализации JPA?
Реализаций JPA больше, чем только Hibernate. В первую очередь на ум приходят EclipseLink (см. Hibernate vs 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-фреймворки — это зрелое и сложное программное обеспечение. Главная опасность при их использовании — думать, что при использовании какой-либо из реализаций JPA вам больше не нужно знать SQL.
Да, это правда, что ORM позволяет вам быстро стартануть с маппингом классов на таблицы баз данных. Но при отсутствии базовых знаний о том, как это все работает, позже в проекте вы столкнетесь с серьезными проблемами производительности и поддержки.
Убедитесь, что вы хорошо понимаете, как работает Hibernate, SQL и ваша база данных.
SQL-библиотеки: легковесный подход
Все библиотеки, рассматриваемые далее, имеют более легковесный подход, ориентированный на базы данных (database-first), по сравнению с подходом ORM, ориентированным на Java (java-first).
Они хорошо работают, если у вас есть существующая (легаси) база данных или вы начинаете новый проект с нуля и создаете базу данных перед написанием Java-классов.
jOOQ
jOOQ — популярная библиотека от Лукаса Эдера (Lukas Eder). Лукас также ведет очень интересный блог обо всем, что касается SQL, баз данных и Java.
По сути, работа с jOOQ сводится к следующему:
Для подключения к базе данных и создания классов Java, которые представляют ваши таблицы и столбцы базы данных, используется генератор кода jOOQ.
Для написания SQL-запросов вместо простых строк с использованием голого JDBC используются эти сгенерированные классы.
jOOQ превращает эти классы и запросы в SQL, выполняет его и возвращает результат.
Итак, представьте, что вы настроили генератор кода 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-операциями, маппингом между POJO и записями базы данных.
Он также помогает получить доступ ко всему функционалу вашей базы данных (оконные функции, pivot, flashback-запросы, OLAP, хранимые процедуры и прочее, специфическое для вашей БД).
Более подробное введение вы можете посмотреть в этом небольшом руководстве по jOOQ.
MyBatis
MyBatis — еще один популярный и активно поддерживаемый проект для решений database-first. MyBatis — это форк IBATIS 3.0, который сейчас находится в Apache Attic.
Для работы с MyBatis используется SQLSessionFactory (не путайте с SessionFactory из Hibernate). После создания SQLSessionFactory вы можете выполнять SQL-запросы к базе данных. Сами SQL-запросы находятся либо в файлах XML, либо в аннотациях интерфейсов.
Давайте посмотрим на пример с аннотациями:
package org.mybatis.example;
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectUser(int id);
}
Соответствующая XML-конфигурация:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.UserMapper">
<select id="selectUser" resultType="User">
select * from users where id = #{id}
</select>
</mapper>
Далее можно использовать этот интерфейс следующим образом:
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUser(1);
MyBatis также имеет встроенные функции маппинга, то есть он может преобразовывать данные из таблицы в объект User. Но работает это только для простых случаев, когда, например, имена столбцов совпадают. В противном случае вам придется конфигурировать маппинг самостоятельно в XML-файле конфигурации.
Также MyBatis уделяет довольно много внимания возможностям по работе с динамическим SQL, которые, по сути, являются основанными на XML способами создания сложных динамических SQL-запросов (что-то вроде if-else-when прямо внутри SQL-запроса).
Jdbi
Jdbi — это небольшая библиотека поверх JDBC, которая упрощает доступ к базе данных. Это один из наиболее низкоуровневых SQL-фреймворков. Ее API представлено двумя вариантами.
Во-первых, Fluent API:
Jdbi jdbi = Jdbi.create("jdbc:h2:mem:test"); // (H2 in-memory database)
List<User> users = jdbi.withHandle(handle -> {
handle.execute("CREATE TABLE user (id INTEGER PRIMARY KEY, name VARCHAR)");
// Named parameters from bean properties
handle.createUpdate("INSERT INTO user(id, name) VALUES (:id, :name)")
.bindBean(new User(3, "David"))
.execute();
// Easy mapping to any type
return handle.createQuery("SELECT * FROM user ORDER BY name")
.mapToBean(User.class)
.list();
});
Во-вторых, декларативный API:
// Define your own declarative interface
public interface UserDao {
@SqlUpdate("CREATE TABLE user (id INTEGER PRIMARY KEY, name VARCHAR)")
void createTable();
@SqlUpdate("INSERT INTO user(id, name) VALUES (:id, :name)")
void insertBean(@BindBean User user);
@SqlQuery("SELECT * FROM user ORDER BY name")
@RegisterBeanMapper(User.class)
List<User> listUsers();
}
public class MyApp {
public static void main(String[] args) {
Jdbi jdbi = Jdbi.create("jdbc:h2:mem:test");
jdbi.installPlugin(new SqlObjectPlugin());
List<User> userNames = jdbi.withExtension(UserDao.class, dao -> {
dao.createTable();
dao.insertBean(new User(3, "David"));
return dao.listUsers();
});
}
}
fluent-jdbc
fluent-jdbc — библиотека, аналогичная Jdbi. Удобная обертка над чистым JDBC. Больше примеров вы можете посмотреть на ее домашней странице.
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 немного слабоват с точки зрения документации, но это отличная небольшая библиотека, которая помогает вам мапить ResultSet из JDBC и Record из jOOQ на POJO. На самом деле, поскольку это "всего лишь" маппер, он интегрируется с большинством фреймворков баз данных, упомянутых в этом руководстве начиная с JDBC, jOOQ, queryDSL, JDBI и заканчивая Spring JDBC.
Давайте рассмотрим пример использования с JDBC:
// will map the resultset to User POJOs
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); //prints out all user pojos
}
Spring JDBC и Spring Data
Вселенная Spring на самом деле представляет собой огромную экосистему, поэтому вам не следует начинать сразу со Spring Data, а лучше сначала разобраться с более низкоуровневыми вещами в Spring.
Spring JDBC Template
JDBCTemplate
— один из старейших вспомогательных классов в Spring (точнее, в зависимости spring-jdbc). Он существует с 2001 года, и его не следует путать с Spring Data JDBC.
Он представляет собой обертку над JDBC-соединениями и предлагает удобную обработку ResultSet, соединений, ошибок, а также обеспечивает интеграцию с @Transactional-фреймворком в Spring.
Он представлен двумя вариантами: 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)
// named JDBC template with :named parameters
NamedParameterJdbcTemplate namedTemplate = new NamedParameterJdbcTemplate(datasource);
SqlParameterSource namedParameters = new MapSqlParameterSource().addValue("id", 1);
namedParameterJdbcTemplate.queryForObject( (4)
"SELECT * FROM USERS WHERE ID = :id", namedParameters, String.class);
Сначала мы выполняем SQL для создания таблицы. Обратите внимание, что по сравнению с обычным JDBC здесь не нужно обрабатывать SQLException, так как Spring преобразует их в
RuntimeException
.Используем знак вопроса (
?
) для параметров.Используем
RowMapper
для преобразования простого JDBC ResultSet в POJO нашего проекта.NamedParameterJdbcTemplate
позволяет ссылаться на параметры в SQL-запросе по имени (например,:id
), а не по знаку вопроса (?
).
Посмотрев на этот код, вы увидите, что JdbcTemplate
на самом деле является всего лишь изящной оберткой вокруг JDBC API.
Как работает управление транзакциями в Spring
Одна из сильных сторон Spring — возможность объявлять транзакции баз данных с помощью аннотации @Transactional.
Эта аннотация не только избавляет вас от необходимости самостоятельно открывать, обрабатывать и закрывать соединения с БД, но также может использоваться совместно с внешними библиотеками, такими как Hibernate, jOOQ или любой другой реализацией JPA, обрабатывая транзакции для них.
Давайте еще раз посмотрим на наш предыдущий пример с JPA, где мы вручную открывали и фиксировали транзакцию через EntityManager (помните, EntityManager — это на самом деле просто Hibernate Session, который представляет собой соединение 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 код становится следующим:
Кажется, код читается гораздо приятнее. Очевидно, что обработка транзакций содержит в себе гораздо больше, но в этой статье мы не будем углубляться в детали.
Если вам интересно разобраться с транзакциями, то вы можете взглянуть на мою электронную книгу Java Database Connections & Transactions ("Соединения и транзакции с базами данных в Java"). В ней вы найдете много примеров кода и упражнений, которые помогут вам научиться правильно работать с транзакциями.
Spring Data JPA
Наконец, пришло время взглянуть на Spring Data, миссией которого является "предоставление модели разработки Spring для доступа к данным, сохраняя при этом особенности хранилищ". Ух, что это за заявление о миссии из списка Fortune 500. Что это на самом деле означает?
Spring Data — это общее название множества подпроектов:
Два самых популярных, которые мы рассмотрим в этой статье: Spring Data JDBC и Spring Data JPA.
И множество других, таких как 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-операции (save, delete, findBy) бесплатно без необходимости писать дополнительный код.
Как написать свои Spring Data JPA-репозитории
Давайте посмотрим на несколько примеров кода. Предполагая, что у вас в classpath
есть соответствующие spring-data-{jdbc|jpa}.jar
. С добавлением небольшой конфигурации вы могли бы написать следующий код:
import org.springframework.data.jpa.repository.JpaRepository;
import com.marcobehler.domain.User;
public interface MyUserRepository extends JpaRepository<User, Long> {
// JpaRepository contains all of these methods, you do not have to write them yourself!
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
// and many more...that you can execute without implementing, because Spring Data JPA will
// automatically generate an implementation for you - at runtime
}
Ваш репозиторий просто должен наследоваться от 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 при запуске будет читать имя метода и преобразовывать его в соответствующий запрос при каждом выполнении этого метода. Для примера выше генерируется следующий SQL: "select * from Users where email_address = :emailAddress and lastName = :lastName
".
Spring Data JDBC работает по другому (помимо наследования от другого интерфейса CrudRepository
). В нем запрос пишется вручную.
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: резюме
Можно сделать несколько выводов:
По своей сути Spring Data — это все еще про простой доступ к данным и, следовательно, про репозитории и запросы. Он понимает аннотации javax.persistence и на основе этих аннотаций генерирует для вас DAO.
Spring Data JPA — это удобная библиотека поверх JPA / Hibernate. Дело не в том, что это разные библиотеки, а скорее в том, что они работают вместе. Spring Data JPA позволяет вам писать супер-простые JPA-репозитории, при этом предоставляя доступ ко всему функционалу вашего ORM.
Spring Data JDBC — это удобная библиотека поверх JDBC. Она позволяет вам писать репозитории на основе JDBC, когда не нужны возможности полноценного ORM (кэширование, ленивая загрузка, …). Это дает больше контроля и меньше магии за кулисами.
И самое главное, Spring Data прекрасно интегрируется с другими Spring-проектами и это очевидный выбор для Spring Boot-проектов.
Опять же, это руководство дает только краткий обзор того, что такое Spring Data, более подробную информацию смотрите в официальной документации.
Что выбрать
К этому моменту вы можете почувствовать себя немного подавленным. Куча разных библиотек и вариантов. Но все это можно подытожить несколькими рекомендациями (как вы, возможно, уже догадались, единственно верного решения не существует):
Независимо от того, какую библиотеку для доступа к базам данных вы будете использовать, убедитесь, что вы хорошо разбираетесь в SQL и базах данных (что часто отсутствует у java-разработчиков).
Выберите библиотеку с активным сообществом, хорошей документацией и регулярными релизами.
Досконально изучите используемые фреймворки для доступа к данным, т.е. потратьте время на чтение этих 608 страниц.
Ваш проект отлично будет работать как с обычным Hibernate, так и с Hibernate, завернутым в JPA.
Также он будет себя чувствовать отлично с jOOQ или любой из других упомянутых database-first библиотек.
Вы также можете комбинировать библиотеки. Например, JPA-реализацию и jOOQ или голый JDBC. Или добавить удобства, используя такие библиотеки как QueryDSL.
Узнать подробнее о курсе "Java Developer. Professional".
Принять участие в открытом уроке на тему "Введение в Spring Data Jdbc".