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:
Нам также нужен подходящий драйвер 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) для наших классов предметной области. Плагин JDBI SqlObject предлагает простой способ реализации этих классов, который напоминает способ работы с этой темой в Spring Data.
Нам просто нужно определить интерфейс с несколькими аннотациями, и автоматически JDBI будет обрабатывать все низкоуровневые вещи, такие как обработка соединений JDBC и создание/удаление операторов и ResultSet
s :
@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 ищет ресурс, используя полное имя и метод интерфейса. Например, при заданном FQN интерфейса abcFoo с
методом 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 в нашем приложении, мы должны создать их экземпляры, используя один из фабричных методов, доступных в 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 запрашивает новый Connection
, он получает существующий, связанный с текущей транзакцией, тем самым интегрируя свой жизненный цикл с другими ресурсами, которые могут быть зарегистрированы.
8. Заключение
В этой статье мы показали, как быстро интегрировать JDBI в приложение Spring Boot . Это мощная комбинация в сценариях, когда мы по какой-то причине не можем использовать Spring Data JPA, но все же хотим использовать все другие функции, такие как управление транзакциями, интеграция и т. д.
Как обычно, весь код доступен на GitHub .