Usando JDBI com Spring Boot
1. Introdução
Em aprevious tutorial, cobrimos o básico deJDBI,an open-source library for relational database access que remove muito do código clichê relacionado ao uso direto do JDBC.
This time, we’ll see how we can use JDBI in a Spring Boot application. Também cobriremos alguns aspectos desta biblioteca que a tornam uma boa alternativa ao Spring Data JPA em alguns cenários.
2. Configuração do Projeto
Em primeiro lugar, vamos adicionar as dependências JDBI apropriadas ao nosso projeto. This time, we’ll use JDBI’s Spring integration plugin, which brings all required core dependencies. Também traremos o plug-in SqlObject, que adiciona alguns recursos extras ao JDBI base que usaremos em nossos exemplos:
org.springframework.boot
spring-boot-starter-jdbc
2.1.8.RELEASE
org.jdbi
jdbi3-spring4
3.9.1
org.jdbi
jdbi3-sqlobject
3.9.1
A versão mais recente desses artefatos pode ser encontrada no Maven Central:
Também precisamos de um driver JDBC adequado para acessar nosso banco de dados. Neste artigo, usaremosH2, então devemos adicionar seu driver à nossa lista de dependências também:
com.h2database
h2
1.4.199
runtime
3. Instanciação e configuração do JDBI
Já vimos em nosso artigo anterior que precisamos de uma instânciaJdbi como nosso ponto de entrada para acessar a API do JDBI. Como estamos no mundo Spring, faz sentido disponibilizar uma instância dessa classe como um bean.
Vamos aproveitar os recursos de configuração automática do Spring Boot para inicializar umDataSourcee passá-lo para um método anotado em@Bean que criará nossa instância globalJdbi.
Também passaremos quaisquer plug-ins descobertos e instâncias deRowMapper para este método para que sejam registrados antecipadamente:
@Configuration
public class JdbiConfiguration {
@Bean
public Jdbi jdbi(DataSource ds, List jdbiPlugins, List> 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;
}
}
Aqui, estamos usando umDataSource disponível e envolvendo-o em umTransactionAwareDataSourceProxy. We need this wrapper in order to integrate Spring-managed transactions with JDBI, como veremos mais tarde.
O registro de plug-ins e instâncias do RowMapper é simples. Tudo o que precisamos fazer é chamarinstallPlugineinstallRowMapper para cadaJdbiPlugineRowMapper, disponível, respectivamente. Depois disso, temos uma instânciaJdbi totalmente configurada que podemos usar em nosso aplicativo.
4. Domínio de exemplo
Nosso exemplo usa um modelo de domínio muito simples que consiste em apenas duas classes:CarMakereCarModel. Como o JDBI não requer anotações em nossas classes de domínio, podemos usar POJOs simples:
public class CarMaker {
private Long id;
private String name;
private List 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. Criando DAOs
Agora, vamos criar objetos de acesso a dados (DAOs) para nossas classes de domínio. O plugin JDBI SqlObject oferece uma maneira fácil de implementar essas classes, que se assemelha à maneira do Spring Data de lidar com este assunto.
Só precisamos definir uma interface com algumas anotações e, automagicamente,JDBI will handle all low-level stuff such as handling JDBC connections and creating/disposing of statements and ResultSets:
@UseClasspathSqlLocator
public interface CarMakerDao {
@SqlUpdate
@GetGeneratedKeys
Long insert(@BindBean CarMaker carMaker);
@SqlBatch("insert")
@GetGeneratedKeys
List bulkInsert(@BindBean List carMakers);
@SqlQuery
CarMaker findById(Long id);
}
@UseClasspathSqlLocator
public interface CarModelDao {
@SqlUpdate
@GetGeneratedKeys
Long insert(@BindBean CarModel carModel);
@SqlBatch("insert")
@GetGeneratedKeys
List bulkInsert(@BindBean List models);
@SqlQuery
CarModel findByMakerIdAndSku(@Bind("makerId") Long makerId, @Bind("sku") String sku );
}
Essas interfaces são fortemente anotadas, então vamos dar uma olhada rápida em cada uma delas.
5.1. @UseClasspathSqlLocator
The @UseClasspathSqlLocator annotation tells JDBI that actual SQL statements associated with each method are located at external resource files. Por padrão, o JDBI pesquisará um recurso usando o nome e método totalmente qualificado da interface. Por exemplo, dado o FQN de uma interface dea.b.c.Foo com um métodofindById(), o JDBI irá procurar um recurso chamadoa/b/c/Foo/findById.sql.
Este comportamento padrão pode ser substituído por qualquer método, passando o nome do recurso como o valor para a anotação@SqlXXX.
5.2. @SqlUpdate/@SqlBatch/@SqlQuery
We use the @SqlUpdate, @SqlBatch, and @SqlQuery annotations to mark data-access methods, which will be executed using the given parameters. Essas anotações podem assumir um valor de string opcional, que será a instrução SQL literal a ser executada - incluindo qualquer parâmetro nomeado - ou quando usado com@UseClasspathSqlLocator, o nome do recurso que o contém.
Os métodos anotados em@SqlBatch podem ter argumentos do tipo coleção e executar a mesma instrução SQL para cada item disponível em uma única instrução em lote. Em cada uma das classes DAO acima, temos um métodobulkInsert que ilustra seu uso. A principal vantagem do uso de instruções em lote é o desempenho adicional que podemos obter ao lidar com grandes conjuntos de dados.
5.3. @GetGeneratedKeys
Como o nome indica,the @GetGeneratedKeys annotation allows us to recover any generated keys as a result of successful execution. É usado principalmente em instruçõesinsert onde nosso banco de dados irá gerar automaticamente novos identificadores e precisamos recuperá-los em nosso código.
5.4. @BindBean/@Bind
We use @BindBean and @Bind annotations to bind the named parameters in the SQL statement with method parameters. @BindBean usa convenções de bean padrão para extrair propriedades de um POJO - incluindo as aninhadas. @Bind usa o nome do parâmetro ou o valor fornecido para mapear seu valor para um parâmetro nomeado.
6. Usando DAOs
Para usar esses DAOs em nosso aplicativo, precisamos instancia-los usando um dos métodos de fábrica disponíveis no JDBI.
Em um contexto Spring, a maneira mais simples é criar um bean para cada DAO usando o métodoonDemand:
@Bean
public CarMakerDao carMakerDao(Jdbi jdbi) {
return jdbi.onDemand(CarMakerDao.class);
}
@Bean
public CarModelDao carModelDao(Jdbi jdbi) {
return jdbi.onDemand(CarModelDao.class);
}
The onDemand-created instance is thread-safe and uses a database connection only during a method call. Desde o JDBI, usaremos oTransactionAwareDataSourceProxy,this means we can use it seamlessly with Spring-managed transactions fornecido.
Embora simples, a abordagem que usamos aqui está longe de ser ideal quando temos que lidar com mais do que algumas tabelas. Uma maneira de evitar escrever esse tipo de código clichê é criar umBeanFactory. personalizado. No entanto, a descrição de como implementar tal componente está além do escopo deste tutorial.
7. Serviços Transacionais
Vamos usar nossas classes DAO em uma classe de serviço simples que cria algumas instâncias deCarModel, dado umCarMaker preenchido com modelos. Primeiro, vamos verificar se oCarMaker fornecido foi salvo anteriormente, salvando-o no banco de dados se necessário. Então, vamos inserir cadaCarModel um por um.
If there’s a unique key violation (or some other error) at any point, the whole operation must fail and a full rollback should be performed.
O JDBI fornece uma anotação@Transaction,but we can’t use it here, pois não tem conhecimento de outros recursos que possam estar participando da mesma transação de negócios. Em vez disso, usaremos a anotação@Transactional do Spring em nosso método de serviço:
@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();
}
}
A implementação da operação em si é bastante simples: estamos usando a convenção padrão de que um valornull no campoid implica que essa entidade ainda não foi persistida no banco de dados. Se for esse o caso, usamos a instânciaCarMakerDao injetada no construtor para inserir um novo registro no banco de dados e obter osid. gerados
Assim que tivermos o id deCarMaker, iteramos sobre os modelos, definindo o campomakerId para cada um antes de salvá-lo no banco de dados.
All those database operations will happen using the same underlying connection and will be part of the same transaction. O truque aqui está na maneira como vinculamos o JDBI ao Spring usandoTransactionAwareDataSourceProxye criandoonDemand DAOs. Quando o JDBI solicitar um novoConnection, ele obterá um existente associado à transação atual, integrando assim seu ciclo de vida a outros recursos que possam estar cadastrados.
8. Conclusão
In this article, we’ve shown how to quickly integrate JDBI into a Spring Boot application. Esta é uma combinação poderosa em cenários onde não podemos usar Spring Data JPA por algum motivo, mas ainda queremos usar todos os outros recursos, como gerenciamento de transações, integração e assim por diante.
Como de costume, todo o código está disponívelover at GitHub.