Перейти к основному содержимому

Путеводитель по Дждби

· 9 мин. чтения

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, мы должны, как минимум:

  1. создать запрос
  2. выбрать, как представлять каждую строку
  3. перебрать результаты

Теперь мы рассмотрим каждый из пунктов выше.

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, поэтому его легко импортировать и запускать как есть.