«1. Введение

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

На этот раз мы увидим, как мы можем использовать JDBI в приложении Spring Boot. Мы также рассмотрим некоторые аспекты этой библиотеки, которые делают ее хорошей альтернативой Spring Data JPA в некоторых сценариях.

2. Настройка проекта

Прежде всего, давайте добавим в наш проект соответствующие JDBI-зависимости. На этот раз мы будем использовать плагин интеграции JDBI Spring, который предоставляет все необходимые основные зависимости. Мы также добавим подключаемый модуль SqlObject, который добавляет некоторые дополнительные функции в базовый JDBI, которые мы будем использовать в наших примерах:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    <version>2.1.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-spring4</artifactId>
    <version>3.9.1</version>
</dependency>
<dependency>
    <groupId>org.jdbi</groupId>
    <artifactId>jdbi3-sqlobject</artifactId>
    <version>3.9.1</version> 
</dependency>

Последнюю версию этих артефактов можно найти в Maven Central:

    Spring Boot Start JDBC JDBI Spring Integration JDBI SqlObject Plugin

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

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.199</version>
    <scope>runtime</scope>
</dependency>

3. Создание и настройка JDBI

В предыдущей статье мы уже видели, что нам нужен Экземпляр Jdbi в качестве нашей точки входа для доступа к API JDBI. Поскольку мы находимся в мире Spring, имеет смысл сделать экземпляр этого класса доступным в виде bean-компонента.

Мы будем использовать возможности автоматической настройки Spring Boot для инициализации DataSource и передачи его методу с аннотацией @Bean, который создаст наш глобальный экземпляр Jdbi.

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

@Configuration
public class JdbiConfiguration {
    @Bean
    public Jdbi jdbi(DataSource ds, List<JdbiPlugin> jdbiPlugins, List<RowMapper<?>> rowMappers) {        
        TransactionAwareDataSourceProxy proxy = new TransactionAwareDataSourceProxy(ds);        
        Jdbi jdbi = Jdbi.create(proxy);
        jdbiPlugins.forEach(plugin -> jdbi.installPlugin(plugin));
        rowMappers.forEach(mapper -> jdbi.registerRowMapper(mapper));       
        return jdbi;
    }
}

Здесь мы используем доступный DataSource и упаковываем его в TransactionAwareDataSourceProxy. Нам нужна эта оболочка для интеграции транзакций, управляемых Spring, с JDBI, как мы увидим позже.

Регистрация плагинов и экземпляров RowMapper проста. Все, что нам нужно сделать, это вызвать installPlugin и installRowMapper для каждого доступного JdbiPlugin и RowMapper соответственно. После этого у нас есть полностью настроенный экземпляр Jdbi, который мы можем использовать в нашем приложении.

4. Образец предметной области

В нашем примере используется очень простая модель предметной области, состоящая всего из двух классов: CarMaker и CarModel. Поскольку JDBI не требует каких-либо аннотаций для наших классов предметной области, мы можем использовать простые POJO:

public class CarMaker {
    private Long id;
    private String name;
    private List<CarModel> models;
    // getters and setters ...
}

public class CarModel {
    private Long id;
    private String name;
    private Integer year;
    private String sku;
    private Long makerId;
    // getters and setters ...
}

5. Создание DAO

Теперь давайте создадим объекты доступа к данным (DAO) для наших классов предметной области. Плагин JDBI SqlObject предлагает простой способ реализации этих классов, который напоминает способ работы с этой темой в Spring Data.

Нам просто нужно определить интерфейс с несколькими аннотациями, и JDBI автоматически будет обрабатывать все низкоуровневые вещи, такие как обработка соединений JDBC и создание/удаление операторов и наборов результатов:

@UseClasspathSqlLocator
public interface CarMakerDao {
    @SqlUpdate
    @GetGeneratedKeys
    Long insert(@BindBean CarMaker carMaker);
    
    @SqlBatch("insert")
    @GetGeneratedKeys
    List<Long> bulkInsert(@BindBean List<CarMaker> carMakers);
    
    @SqlQuery
    CarMaker findById(Long id);
}

@UseClasspathSqlLocator
public interface CarModelDao {    
    @SqlUpdate
    @GetGeneratedKeys
    Long insert(@BindBean CarModel carModel);

    @SqlBatch("insert")
    @GetGeneratedKeys
    List<Long> bulkInsert(@BindBean List<CarModel> models);

    @SqlQuery
    CarModel findByMakerIdAndSku(@Bind("makerId") Long makerId, @Bind("sku") String sku );
}

Эти интерфейсы сильно загружены. аннотированы, поэтому давайте кратко рассмотрим каждый из них.

5.1. @UseClasspathSqlLocator

Аннотация @UseClasspathSqlLocator сообщает JDBI, что фактические операторы SQL, связанные с каждым методом, находятся во внешних файлах ресурсов. По умолчанию JDBI ищет ресурс, используя полное имя и метод интерфейса. Например, при наличии полного доменного имени интерфейса a.b.c.Foo с методом findById() JDBI будет искать ресурс с именем a/b/c/Foo/findById.sql.

Это поведение по умолчанию можно переопределить для любого заданного метода, передав имя ресурса в качестве значения для аннотации @SqlXXX.

5.2. @SqlUpdate/@SqlBatch/@SqlQuery

Мы используем аннотации @SqlUpdate, @SqlBatch и @SqlQuery для обозначения методов доступа к данным, которые будут выполняться с использованием заданных параметров. Эти аннотации могут принимать необязательное строковое значение, которое будет буквальным оператором SQL для выполнения, включая любые именованные параметры, или, при использовании с @UseClasspathSqlLocator, именем содержащего его ресурса.

«Аннотированные @SqlBatch методы могут иметь аргументы, подобные коллекции, и выполнять один и тот же оператор SQL для каждого доступного элемента в одном пакетном операторе. В каждом из вышеперечисленных классов DAO у нас есть метод bulkInsert, иллюстрирующий его использование. Основным преимуществом использования пакетных операторов является дополнительная производительность, которую мы можем достичь при работе с большими наборами данных.

5.3. @GetGeneratedKeys

Как следует из названия, аннотация @GetGeneratedKeys позволяет восстановить любые сгенерированные ключи в результате успешного выполнения. В основном он используется в операторах вставки, где наша база данных автоматически генерирует новые идентификаторы, и нам нужно восстановить их в нашем коде.

5.4. @BindBean/@Bind

Мы используем аннотации @BindBean и @Bind для привязки именованных параметров в инструкции SQL к параметрам метода. @BindBean использует стандартные соглашения bean-компонентов для извлечения свойств из POJO, включая вложенные. @Bind использует имя параметра или предоставленное значение для сопоставления его значения с именованным параметром.

6. Использование DAO

Чтобы использовать эти DAO в нашем приложении, мы должны создать их экземпляры, используя один из фабричных методов, доступных в JDBI.

В контексте Spring самым простым способом является создание bean-компонента для каждого DAO с использованием метода onDemand:

@Bean
public CarMakerDao carMakerDao(Jdbi jdbi) {        
    return jdbi.onDemand(CarMakerDao.class);       
}

@Bean
public CarModelDao carModelDao(Jdbi jdbi) {
    return jdbi.onDemand(CarModelDao.class);
}

Экземпляр, созданный onDemand, является потокобезопасным и использует соединение с базой данных только во время вызова метода. Поскольку JDBI мы будем использовать предоставленный TransactionAwareDataSourceProxy, это означает, что мы можем беспрепятственно использовать его с транзакциями, управляемыми Spring.

Несмотря на простоту, подход, который мы использовали здесь, далек от идеала, когда нам приходится иметь дело с несколькими таблицами. Один из способов избежать написания такого шаблонного кода — создать собственный BeanFactory. Однако описание того, как реализовать такой компонент, выходит за рамки данного руководства.

7. Транзакционные сервисы

Давайте используем наши классы DAO в простом сервисном классе, который создает несколько экземпляров CarModel для данного CarMaker, заполненного моделями. Во-первых, мы проверим, был ли ранее сохранен данный CarMaker, и при необходимости сохраним его в базе данных. Затем мы будем вставлять каждую модель автомобиля одну за другой.

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

JDBI предоставляет аннотацию @Transaction, но мы не можем использовать ее здесь, поскольку она не знает о других ресурсах, которые могут участвовать в той же бизнес-транзакции. Вместо этого мы будем использовать аннотацию Spring @Transactional в нашем сервисном методе:

@Service
public class CarMakerService {
    
    private CarMakerDao carMakerDao;
    private CarModelDao carModelDao;

    public CarMakerService(CarMakerDao carMakerDao,CarModelDao carModelDao) {        
        this.carMakerDao = carMakerDao;
        this.carModelDao = carModelDao;
    }    
    
    @Transactional
    public int bulkInsert(CarMaker carMaker) {
        Long carMakerId;
        if (carMaker.getId() == null ) {
            carMakerId = carMakerDao.insert(carMaker);
            carMaker.setId(carMakerId);
        }
        carMaker.getModels().forEach(m -> {
            m.setMakerId(carMaker.getId());
            carModelDao.insert(m);
        });                
        return carMaker.getModels().size();
    }
}

Сама реализация операции довольно проста: мы используем стандартное соглашение о том, что нулевое значение в поле id подразумевает, что этот объект еще не были сохранены в базе данных. Если это так, мы используем экземпляр CarMakerDao, введенный в конструктор, чтобы вставить новую запись в базу данных и получить сгенерированный идентификатор.

Получив идентификатор CarMaker, мы перебираем модели, устанавливая поле makerId для каждой из них перед сохранением в базе данных.

Все эти операции с базой данных будут выполняться с использованием одного и того же базового соединения и будут частью одной и той же транзакции. Хитрость здесь заключается в том, как мы привязали JDBI к Spring, используя TransactionAwareDataSourceProxy и создавая DAO по требованию. Когда JDBI запрашивает новое соединение, оно получает существующее, связанное с текущей транзакцией, тем самым интегрируя свой жизненный цикл с другими ресурсами, которые могут быть зарегистрированы.

8. Заключение

В этой статье мы показали, как быстро интегрировать JDBI в приложение Spring Boot. Это мощная комбинация в сценариях, когда мы по какой-то причине не можем использовать Spring Data JPA, но все же хотим использовать все другие функции, такие как управление транзакциями, интеграция и т. д.

Как обычно, весь код доступен на GitHub.