R2DBC - Conectividade de banco de dados relacional reativa

R2DBC - Conectividade de banco de dados relacional reativa

1. Visão geral

Neste tutorial, mostraremos como podemos usar o R2DBC para executar operações do banco de dados de maneira reativa .

Para explorar o R2DBC, criaremos um aplicativo Spring WebFlux REST simples que implementa operações CRUD para uma única entidade, usando apenas operações assíncronas para atingir esse objetivo.

2. O que é o R2DBC?

O desenvolvimento reativo está em ascensão, com novas estruturas surgindo todos os dias e as existentes, com crescente adoção. No entanto, um grande problema no desenvolvimento reativo é o fato de que o acesso ao banco de dados no mundo Java/JVM permanece basicamente síncrono . Essa é uma conseqüência direta da maneira como o JDBC foi projetado e levou a alguns hacks feios para adaptar essas duas abordagens fundamentalmente diferentes.

Para atender à necessidade de acesso assíncrono ao banco de dados no território Java, surgiram dois padrões. O primeiro, ADBC ​​(API de acesso assíncrono ao banco de dados), é suportado pela Oracle, mas, até o momento, parece estar um pouco parado, sem uma linha do tempo clara.

O segundo, que abordaremos aqui, é o R2DBC (Reactive Relational Database Connectivity), um esforço da comunidade liderado por uma equipe da Pivotal e outras empresas. Este projeto, que ainda está na versão beta, mostrou mais vitalidade e já fornece drivers para os bancos de dados Postgres, H2 e MSSQL.

3. Configuração do Projeto

O uso do R2DBC em um projeto exige que adicionemos dependências à API principal e um driver adequado. Em nosso exemplo, usaremos H2, portanto, isso significa apenas duas dependências:

<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-spi</artifactId>
    <version>0.8.0.M7</version>
</dependency>
<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-h2</artifactId>
    <version>0.8.0.M7</version>
</dependency>

O Maven Central ainda não possui artefatos R2DBC no momento, portanto, também precisamos adicionar alguns repositórios do Spring ao nosso projeto:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
   </repository>
   <repository>
       <id>spring-snapshots</id>
       <name>Spring Snapshots</name>
       <url>https://repo.spring.io/snapshot</url>
       <snapshots>
           <enabled>true</enabled>
       </snapshots>
    </repository>
</repositories>

4. Configuração de fábrica de conexões

A primeira coisa que precisamos fazer para acessar um banco de dados usando o R2DBC é criar um objeto ConnectionFactory , que desempenha um papel semelhante ao DataSource do JDBC. A maneira mais direta de criar um _ConnectionFactory é através da classe ConnectionFactories.

Esta classe possui métodos estáticos que pegam um objeto ConnectionFactoryOptions e retornam um ConnectionFactory. Como nós precisaremos apenas de uma única instância de nossa ConnectionFactory, vamos criar um _ @ Bean_ que possamos usar posteriormente por injeção sempre que precisarmos:

@Bean
public ConnectionFactory connectionFactory(R2DBCConfigurationProperties properties) {
    ConnectionFactoryOptions baseOptions = ConnectionFactoryOptions.parse(properties.getUrl());
    Builder ob = ConnectionFactoryOptions.builder().from(baseOptions);
    if (!StringUtil.isNullOrEmpty(properties.getUser())) {
        ob = ob.option(USER, properties.getUser());
    }
    if (!StringUtil.isNullOrEmpty(properties.getPassword())) {
        ob = ob.option(PASSWORD, properties.getPassword());
    }
    return ConnectionFactories.get(ob.build());
}

Aqui, pegamos as opções recebidas de uma classe auxiliar decorada com a anotação _ @ ConfigurationProperties_ e preenchemos nossa instância ConnectionFactoryOptions. Para preenchê-lo, o R2DBC implementa um padrão do construtor com um único método de option que utiliza uma Option e um valor.

O R2DBC define várias opções conhecidas, como USERNAME e PASSWORD que usamos acima. Outra maneira de definir essas opções é passar uma cadeia de conexão para o método parse () _ da classe _ConnectionFactoryOptions.

Aqui está um exemplo de um URL de conexão R2DBC típico:

r2dbc:h2:mem://./testdb

Vamos quebrar essa string em seus componentes:

  • r2dbc: identificador de esquema fixo para URLs R2DBC - outro esquema válido é rd2bcs, usado para conexões protegidas por SSL

  • h2: identificador de driver usado para localizar o connection factory apropriado

  • mem: protocolo específico do driver - no nosso caso, isso corresponde a um banco de dados na memória *//./testdb: sequência específica do driver, geralmente contendo host, banco de dados e outras opções adicionais.

Assim que tivermos nossa opção pronta, passamos para o método de fábrica estático _get () _ para criar nosso feijão ConnectionFactory .

5. Executando declarações

Da mesma forma que o JDBC, o uso do R2DBC é principalmente sobre o envio de instruções SQL para o banco de dados e o processamento de conjuntos de resultados.* No entanto, como o R2DBC é uma API reativa, depende muito dos tipos de fluxos reativos, como Publisher e Subscriber *.

Usar esses tipos diretamente é um pouco complicado, então usaremos tipos de reatores de projeto como Mono e Flux que nos ajudam a escrever código mais limpo e conciso.

Nas próximas seções, veremos como implementar tarefas relacionadas ao banco de dados criando uma classe DAO reativa para uma classe Account simples. Esta classe contém apenas três propriedades e possui uma tabela correspondente em nosso banco de dados:

public class Account {
    private Long id;
    private String iban;
    private BigDecimal balance;
   //... getters and setters omitted
}

5.1. Obtendo uma conexão

Antes de podermos enviar quaisquer declarações ao banco de dados, precisamos de uma instância Connection . Já vimos como criar um ConnectionFactory, por isso não é surpresa que vamos usá-lo para obter um Connection. O que devemos lembrar é que agora, em vez de obter uma Connection regular, o que obtemos é um Publisher de uma única Connection.

Nosso ReactiveAccountDao, _ que é um Spring regular _ @ Component, obtém seu ConnectionFactory por injeção de construtor, portanto está prontamente disponível nos métodos manipuladores.

Vamos dar uma olhada nas primeiras linhas do método findById () _ para ver como recuperar e começar a usar um _Connection:

public Mono<Account>> findById(Long id) {
    return Mono.from(connectionFactory.create())
      .flatMap(c ->
         //use the connection
      )
     //... downstream processing omitted
}

Aqui, estamos adaptando o Publisher retornado de nosso ConnectionFactory para um Mono que é a fonte inicial do nosso fluxo de eventos.

5.1. Preparando e enviando declarações

Agora que temos uma Connection, vamos usá-la para criar uma Statement e vincular um parâmetro a ela:

.flatMap( c ->
    Mono.from(c.createStatement("select id,iban,balance from Account where id = $1")
      .bind("$1", id)
      .execute())
      .doFinally((st) -> close(c))
 )

O método do Connection createStatement usa uma string de consulta SQL, que pode opcionalmente ter marcadores de ligação - chamados de "marcadores" https://r2dbc.io/spec/0.8.0.M8/spec/html/#statements.parameterized [ nas especificações].

Alguns pontos dignos de nota aqui: primeiro, createStatement é uma operação síncrona , que nos permite usar um estilo fluente para vincular valores à Statement retornada; segundo, e muito importante, a sintaxe do espaço reservado/marcador é específica do fornecedor!

Neste exemplo, estamos usando a sintaxe específica de H2, que usa _ $ n_ para marcar parâmetros. Outros fornecedores podem usar sintaxe diferente, como : param, _ @ Pn_ ou alguma outra convenção. Esse é um aspecto importante ao qual devemos prestar atenção ao migrar o código herdado para esta nova API .

O processo de ligação em si é bastante direto, devido ao padrão API fluente e à digitação simplificada: existe apenas um método _bind () _ sobrecarregado que cuida de todas as conversões de digitação - sujeito às regras do banco de dados, é claro.

O primeiro parâmetro passado para bind () pode ser um ordinal com base em zero que corresponde ao posicionamento do marcador na instrução ou pode ser uma sequência com o marcador real.

Depois de definirmos valores para todos os parâmetros, chamamos _execute () _, que retorna um Publisher of Result objects, que novamente envolvemos em um Mono para processamento adicional. Anexamos um manipulador _doFinally () _ a esse Mono para garantir que fechemos nossa conexão, independentemente de o processamento do fluxo ser concluído normalmente ou não.

5.2. Processando resultados

A próxima etapa do nosso pipeline é responsável por processar objetos Result e gerar um fluxo de _ResponseEntity <Conta> _ instâncias .

Como sabemos que pode haver apenas uma instância com o id fornecido, na verdade, retornaremos um fluxo Mono. A conversão real ocorre dentro da função passada para o método map () _ do _Result recebido:

.map(result -> result.map((row, meta) ->
    new Account(row.get("id", Long.class),
      row.get("iban", String.class),
      row.get("balance", BigDecimal.class))))
.flatMap(p -> Mono.from(p));

O método map () _ do resultado espera uma função que aceite dois parâmetros. O primeiro é um objeto _Row que usamos para reunir valores para cada coluna e preencher uma instância Account . O segundo, meta, é um objeto RowMetadata que contém informações sobre a linha atual, como nomes e tipos de colunas.

A chamada map () _ anterior em nosso pipeline resolve para um _Mono <Producer <Account>> _, mas precisamos retornar um _Mono <Account> _ deste método. Para corrigir isso, adicionamos uma etapa _flatMap () _ final, que adapta o _Producer a um Mono.

5.3. Instruções de lote

O R2DBC também suporta a criação e execução de lotes de instruções, que permitem a execução de várias instruções SQL em uma única chamada execute () . Ao contrário das instruções regulares, as instruções em lote não suportam a ligação e são usadas principalmente por razões de desempenho em cenários como trabalhos de ETL.

Nosso projeto de amostra usa um lote de instruções para criar a tabela Account e inserir alguns dados de teste nela:

@Bean
public CommandLineRunner initDatabase(ConnectionFactory cf) {
    return (args) ->
      Flux.from(cf.create())
        .flatMap(c ->
            Flux.from(c.createBatch()
              .add("drop table if exists Account")
              .add("create table Account(" +
                "id IDENTITY(1,1)," +
                "iban varchar(80) not null," +
                "balance DECIMAL(18,2) not null)")
              .add("insert into Account(iban,balance)" +
                "values('BR430120980198201982',100.00)")
              .add("insert into Account(iban,balance)" +
                "values('BR430120998729871000',250.00)")
              .execute())
            .doFinally((st) -> c.close())
          )
        .log()
        .blockLast();
}

Aqui, usamos o Batch returned from createBatch () _ e adicionamos algumas instruções SQL. Em seguida, enviamos essas instruções para execução usando o mesmo método _execute () _ disponível na interface _Statement.

Nesse caso em particular, não estamos interessados ​​em nenhum resultado - apenas que todas as instruções executam bem. Se precisássemos de algum resultado produzido, tudo o que precisávamos fazer era adicionar uma etapa posterior nesse fluxo para processar os objetos Result emitidos.

6. Transações

O último tópico que abordaremos neste tutorial são transações. Como deveríamos esperar agora, gerenciamos transações como no JDBC, ou seja, usando métodos disponíveis no objeto Connection .

Como antes, a principal diferença é que agora todos os métodos relacionados à transação são assíncronos , retornando um Publisher que devemos adicionar ao nosso fluxo nos pontos apropriados.

Nosso projeto de amostra usa uma transação na implementação do método _createAccount () _:

public Mono<Account> createAccount(Account account) {
    return Mono.from(connectionFactory.create())
      .flatMap(c -> Mono.from(c.beginTransaction())
        .then(Mono.from(c.createStatement("insert into Account(iban,balance) values($1,$2)")
          .bind("$1", account.getIban())
          .bind("$2", account.getBalance())
          .returnGeneratedValues("id")
          .execute()))
        .map(result -> result.map((row, meta) ->
            new Account(row.get("id", Long.class),
              account.getIban(),
              account.getBalance())))
        .flatMap(pub -> Mono.from(pub))
        .delayUntil(r -> c.commitTransaction())
        .doFinally((st) -> c.close()));
}

Aqui, adicionamos chamadas relacionadas à transação em dois pontos. Primeiro, logo após obter uma nova conexão do banco de dados, chamamos o beginTransactionMethod () _. Quando sabemos que a transação foi iniciada com sucesso, preparamos e executamos a instrução _inserir.

Desta vez, também usamos o método returnGeneratedValues ​​() _ para instruir o banco de dados a retornar o valor de identidade gerado para este novo _Account. O R2DBC retorna esses valores em um Result contendo uma única linha com todos os valores gerados, que usamos para criar a instância Account.

Mais uma vez, precisamos adaptar o _Mono <Publisher <Account>> _ em um _Mono <Account> _, para adicionar um _flatMap () _ para resolver isso. Em seguida, confirmamos a transação em uma etapa _delayUntil () _. Precisamos disso porque queremos garantir que a Conta retornada já tenha sido confirmada no banco de dados.

Por fim, anexamos uma etapa doFinally a esse pipeline que fecha o Connection quando todos os eventos do Mono retornado são consumidos.

7. Exemplo de uso do DAO

Agora que temos um DAO reativo, vamos usá-lo para criar um aplicativo simples https://www..com/spring-webflux [Spring WebFlux] para mostrar como usá-lo em um aplicativo típico. Como essa estrutura já suporta construções reativas, isso se torna uma tarefa trivial. Por exemplo, vamos dar uma olhada na implementação do método GET:

@RestController
public class AccountResource {
    private final ReactiveAccountDao accountDao;

    public AccountResource(ReactiveAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    @GetMapping("/accounts/{id}")
    public Mono<ResponseEntity<Account>> getAccount(@PathVariable("id") Long id) {
        return accountDao.findById(id)
          .map(acc -> new ResponseEntity<>(acc, HttpStatus.OK))
          .switchIfEmpty(Mono.just(new ResponseEntity<>(null, HttpStatus.NOT_FOUND)));
    }
   //... other methods omitted
}

Aqui, estamos usando o Mono do nosso DAO retornado para construir uma ResponseEntity com o código de status apropriado. Estamos fazendo isso apenas porque queremos um código de status NOT_FOUND (404) quando não houver Account com o ID fornecido.

8. Conclusão

Neste artigo, abordamos os conceitos básicos do acesso reativo ao banco de dados usando o R2DBC. Embora em sua infância, este projeto esteja evoluindo rapidamente, visando uma data de lançamento em algum momento do início de 2020.

Comparado ao ADBA, que definitivamente não fará parte do Java 12, o R2DBC parece ser mais promissor e já fornece drivers para alguns bancos de dados populares - o Oracle é uma ausência notável aqui.

Como de costume, o código-fonte completo usado neste tutorial está disponível over no Github.