«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

Если мы подключаемся к базе данных с помощью 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/eugenp/tutorials')");

assertEquals(1, updateCount);

На самом деле, выполнение просто метод удобства.

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

5. Запрос к базе данных

Наиболее простым выражением, которое выводит результаты из БД, является SQL-запрос.

Чтобы выполнить запрос с дескриптором Jdbi, мы должны как минимум:

  1. create the query
  2. choose how to represent each row
  3. iterate over the results

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

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/eugenp/%");

Обратите внимание, что, в отличие от 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/eugenp/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. применим и для обновлений. url

Операторы выполняются, когда мы вызываем, удивляем, выполняем:

int rows = update.execute();

Как мы уже видели, он возвращает количество затронутых строк.

7.1. Извлечение значений столбца с автоинкрементом

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

Затем мы вызываем не execute, а executeAndReturnGeneratedKeys:

Update update = handle.createUpdate(
  "INSERT INTO PROJECT (NAME, URL) "
  + "VALUES ('tutorials', 'github.com/eugenp/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 автоматически откатывает транзакцию.

handle.inTransaction((Handle h) -> {
    h.execute("...");
    h.commit();
    return true;
});

Как и в случае с дескрипторами, у нас есть специальный метод inTransaction, если мы хотим что-то вернуть из замыкания:

8.1. Ручное управление транзакциями

handle.begin();
// ...
handle.commit();
handle.close();

Хотя в общем случае это не рекомендуется, мы также можем начать и закрыть транзакцию вручную:

9. Выводы и дальнейшее чтение

В этом руководстве мы представили ядро Jdbi: запросы, операторы и транзакции.

Мы упустили некоторые расширенные функции, такие как настраиваемое сопоставление строк и столбцов и пакетная обработка.

Мы также не обсуждали дополнительные модули, особенно расширение SQL Object.

Все подробно представлено в документации Jdbi.