1. Введение
В этой статье мы рассмотрим, как выполнять запросы к реляционной базе данных с помощью jdbi .
Jdbi — это библиотека Java с открытым исходным кодом (лицензия Apache), которая использует лямбда-выражения и отражение для предоставления более удобного интерфейса более высокого уровня, чем JDBC , для доступа к базе данных.
Jdbi, однако, не является ORM; несмотря на то, что у него есть дополнительный модуль сопоставления объектов SQL, у него нет сеанса с прикрепленными объектами, уровня независимости базы данных и любых других наворотов типичного ORM.
2. Настройка JDBI
Jdbi состоит из ядра и нескольких дополнительных модулей.
Для начала нам просто нужно включить модуль ядра в наши зависимости:
<dependencies>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-core</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
В этой статье мы покажем примеры с использованием базы данных HSQL:
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
Мы можем найти последнюю версию jdbi3-core
, HSQLDB и других модулей Jdbi на Maven Central.
3. Подключение к базе данных
Во-первых, нам нужно подключиться к базе данных. Для этого нам нужно указать параметры подключения.
Отправной точкой является класс Jdbi
:
Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", "sa", "");
Здесь мы указываем URL-адрес подключения, имя пользователя и, конечно же, пароль.
3.1. Дополнительные параметры
Если нам нужно предоставить другие параметры, мы используем перегруженный метод, принимающий объект Properties
:
Properties properties = new Properties();
properties.setProperty("username", "sa");
properties.setProperty("password", "");
Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", properties);
В этих примерах мы сохранили экземпляр Jdbi
в локальной переменной. Это потому, что мы будем использовать его для отправки операторов и запросов в базу данных.
На самом деле простой вызов create
не устанавливает никакого соединения с БД. Он просто сохраняет параметры подключения на потом.
3.2. Использование источника данных
Если мы подключаемся к базе данных с помощью DataSource
, как это обычно бывает, мы можем использовать соответствующую перегрузку create :
Jdbi jdbi = Jdbi.create(datasource);
3.3. Работа с ручками
Фактические подключения к базе данных представлены экземплярами класса Handle
.
Самый простой способ работать с дескрипторами и автоматически закрывать их — использовать лямбда-выражения:
jdbi.useHandle(handle -> {
doStuffWith(handle);
});
Мы вызываем useHandle
, когда нам не нужно возвращать значение.
В противном случае мы используем withHandle
:
jdbi.withHandle(handle -> {
return computeValue(handle);
});
Также возможно, хотя и не рекомендуется, вручную открывать дескриптор соединения; в этом случае мы должны закрыть его, когда закончим:
Jdbi jdbi = Jdbi.create("jdbc:hsqldb:mem:testDB", "sa", "");
try (Handle handle = jdbi.open()) {
doStuffWith(handle);
}
К счастью, как мы видим, Handle
реализует Closeable
, поэтому его можно использовать с try-with-resources .
4. Простые утверждения
Теперь, когда мы знаем, как установить соединение, давайте посмотрим, как его использовать.
В этом разделе мы создадим простую таблицу, которую будем использовать на протяжении всей статьи.
Для отправки таких операторов, как создание таблицы
в базу данных, мы используем метод execute
:
handle.execute(
"create table project "
+ "(id integer identity, name varchar(50), url varchar(100))");
execute
возвращает количество строк, на которые повлиял оператор:
int updateCount = handle.execute(
"insert into project values "
+ "(1, 'tutorials', 'github.com/foreach/tutorials')");
assertEquals(1, updateCount);
На самом деле, execute — это просто метод удобства.
Мы рассмотрим более сложные варианты использования в последующих разделах, но перед этим нам нужно научиться извлекать результаты из базы данных.
5. Запрос к базе данных
Наиболее простым выражением, которое выводит результаты из БД, является SQL-запрос.
Чтобы выполнить запрос с дескриптором Jdbi, мы должны, как минимум:
- создать запрос
- выбрать, как представлять каждую строку
- перебрать результаты
Теперь мы рассмотрим каждый из пунктов выше.
5.1. Создание запроса
Неудивительно, что Jdbi представляет запросы как экземпляры класса Query
.
Мы можем получить один из дескриптора:
Query query = handle.createQuery("select * from project");
5.2. Сопоставление результатов
Jdbi абстрагируется от JDBC ResultSet
, который имеет довольно громоздкий API.
Поэтому он предлагает несколько возможностей для доступа к столбцам, полученным в результате запроса или какого-либо другого оператора, который возвращает результат. Сейчас мы рассмотрим самые простые.
Мы можем представить каждую строку как карту:
query.mapToMap();
Ключами карты будут имена выбранных столбцов.
Или, когда запрос возвращает один столбец, мы можем сопоставить его с желаемым типом Java:
handle.createQuery("select name from project").mapTo(String.class);
Jdbi имеет встроенные преобразователи для многих распространенных классов. Те, которые относятся к какой-либо библиотеке или системе баз данных, представлены в отдельных модулях.
Конечно, мы также можем определить и зарегистрировать наши преобразователи. Мы поговорим об этом в следующем разделе.
Наконец, мы можем сопоставить строки с bean-компонентом или другим пользовательским классом. Опять же, мы увидим более продвинутые параметры в специальном разделе.
5.3. Повторение результатов
Как только мы решили, как отображать результаты, вызвав соответствующий метод, мы получаем объект ResultIterable
.
Затем мы можем использовать его для перебора результатов по одной строке за раз.
Здесь мы рассмотрим наиболее распространенные варианты.
Мы можем просто собрать результаты в список:
List<Map<String, Object>> results = query.mapToMap().list();
Или к другому типу коллекции
:
List<String> results = query.mapTo(String.class).collect(Collectors.toSet());
Или мы можем перебирать результаты как поток:
query.mapTo(String.class).useStream((Stream<String> stream) -> {
doStuffWith(stream)
});
Здесь мы явно указали переменную потока
для ясности, но это не обязательно.
5.4. Получение единого результата
В качестве особого случая, когда мы ожидаем или заинтересованы только в одной строке, у нас есть пара специальных методов.
Если нам нужен не более одного результата , мы можем использовать findFirst
:
Optional<Map<String, Object>> first = query.mapToMap().findFirst();
Как мы видим, он возвращает необязательное
значение, которое присутствует только в том случае, если запрос возвращает хотя бы один результат.
Если запрос возвращает более одной строки, возвращается только первая.
Если вместо этого нам нужен один и только один результат , мы используем findOnly
:
Date onlyResult = query.mapTo(Date.class).findOnly();
Наконец, если результатов нет или их больше одного, findOnly
выдает исключение IllegalStateException
.
6. Параметры привязки
Часто запросы имеют фиксированную часть и параметризованную часть. Это имеет ряд преимуществ, в том числе:
- безопасность: избегая конкатенации строк, мы предотвращаем внедрение SQL
- простота: нам не нужно запоминать точный синтаксис сложных типов данных, таких как метки времени
- производительность: статическая часть запроса может быть проанализирована один раз и кэширована
Jdbi поддерживает как позиционные, так и именованные параметры.
Мы вставляем позиционные параметры как вопросительные знаки в запросе или выражении:
Query positionalParamsQuery =
handle.createQuery("select * from project where name = ?");
Вместо этого именованные параметры начинаются с двоеточия:
Query namedParamsQuery =
handle.createQuery("select * from project where url like :pattern");
В любом случае для установки значения параметра мы используем один из вариантов метода привязки
:
positionalParamsQuery.bind(0, "tutorials");
namedParamsQuery.bind("pattern", "%github.com/foreach/%");
Обратите внимание, что, в отличие от JDBC, индексы начинаются с 0.
6.1. Привязка нескольких именованных параметров одновременно
Мы также можем связать вместе несколько именованных параметров, используя объект.
Допустим, у нас есть простой запрос:
Query query = handle.createQuery(
"select id from project where name = :name and url = :url");
Map<String, String> params = new HashMap<>();
params.put("name", "REST with Spring");
params.put("url", "github.com/foreach/REST-With-Spring");
Тогда, например, мы можем использовать карту:
query.bindMap(params);
Или мы можем использовать объект по-разному. Здесь, например, мы связываем объект, который следует соглашению JavaBean:
query.bindBean(paramsBean);
Но мы также можем связать поля или методы объекта; обо всех поддерживаемых параметрах см . документацию Jdbi .
7. Выдача более сложных заявлений
Теперь, когда мы рассмотрели запросы, значения и параметры, мы можем вернуться к операторам и применить те же знания.
Вспомните, что метод execute
, который мы видели ранее, — это просто удобный ярлык.
Фактически, как и запросы, операторы DDL и DML представлены как экземпляры класса Update.
Мы можем получить его, вызвав метод createUpdate
для дескриптора:
Update update = handle.createUpdate(
"INSERT INTO PROJECT (NAME, URL) VALUES (:name, :url)");
Затем в Update
у нас есть все методы привязки, которые есть в Query
, поэтому раздел 6. применяется и для обновлений.
Операторы выполняются, когда мы вызываем, удивляем, выполняем
:
int rows = update.execute();
Как мы уже видели, он возвращает количество затронутых строк.
7.1. Извлечение значений столбца с автоинкрементом
В качестве особого случая, когда у нас есть оператор вставки с автоматически сгенерированными столбцами (обычно с автоматическим приращением или последовательностями), мы можем захотеть получить сгенерированные значения.
Затем мы вызываем не execute
, а executeAndReturnGeneratedKeys
:
Update update = handle.createUpdate(
"INSERT INTO PROJECT (NAME, URL) "
+ "VALUES ('tutorials', 'github.com/foreach/tutorials')");
ResultBearing generatedKeys = update.executeAndReturnGeneratedKeys();
ResultBearing
— это тот же интерфейс, реализованный классом Query
, который мы видели ранее, поэтому мы уже знаем, как его использовать:
generatedKeys.mapToMap()
.findOnly().get("id");
8. Транзакции
Нам нужна транзакция всякий раз, когда нам нужно выполнить несколько операторов как одну атомарную операцию.
Как и в случае с дескрипторами соединения, мы вводим транзакцию, вызывая метод с замыканием:
handle.useTransaction((Handle h) -> {
haveFunWith(h);
});
И, как и в случае с дескрипторами, транзакция автоматически закрывается, когда замыкание возвращается.
Однако мы должны зафиксировать или откатить транзакцию перед возвратом:
handle.useTransaction((Handle h) -> {
h.execute("...");
h.commit();
});
Однако если из замыкания выдается исключение, Jdbi автоматически откатывает транзакцию.
Как и в случае с дескрипторами, у нас есть специальный метод inTransaction
, если мы хотим что-то вернуть из замыкания:
handle.inTransaction((Handle h) -> {
h.execute("...");
h.commit();
return true;
});
8.1. Ручное управление транзакциями
Хотя в общем случае это не рекомендуется, мы также можем начать
и закрыть
транзакцию вручную:
handle.begin();
// ...
handle.commit();
handle.close();
9. Выводы и дальнейшее чтение
В этом руководстве мы представили ядро Jdbi: запросы, операторы и транзакции.
Мы упустили некоторые расширенные функции, такие как настраиваемое сопоставление строк и столбцов и пакетная обработка.
Мы также не обсуждали какие-либо дополнительные модули, особенно расширение SQL Object.
Все подробно представлено в документации Jdbi .
Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub — это проект Maven, поэтому его легко импортировать и запускать как есть.