Inicialização reativa do Java Spring com MongoDB
Teo Wen Jie5 min read • Published Apr 02, 2024 • Updated Apr 02, 2024
APLICATIVO COMPLETO
Spring Boot + Reactive + Spring Data + MongoDB. Unir essas quatro tecnologias pode ser um desafio, especialmente se você está apenas começando. Sem entrar em detalhes de cada uma dessas tecnologias, este tutorial tem como objetivo ajudá-lo a dar um salto inicial em uma base de código funcional com base nessa pilha de tecnologias. Este tutorial apresenta:
- Interagindo com o MongoDB usando ReactiveMongoRepositores.
- Interagindo com o MongoDB usando o ReactiveMongoTemplate.
Esse aplicativo simplificado de saldo de caixa permite fazer chamadas da REST API para:
- Criar ou buscar uma conta.
- Realizar transações em uma conta ou entre duas contas.
Acesse o README do repositório para obter mais detalhes sobre as especificações funcionais. O README também contém instruções de configuração, uso da API e teste. Para clonar o repositório:
1 git clone git@github.com:mongodb-developer/mdb-spring-boot-reactive.git
Vamos fazer um passo a passo lógico de como o código funciona. Eu incluiria trechos de código, mas para reduzir a verbosidade, excluirei linhas de código que não são essenciais para nossa compreensão de como o código funciona.
Esta seção mostra como você pode executar operações de criação e leitura com
ReactiveMongoRepository
.1 2 public class AccountController { 3 //... 4 5 public Mono<Account> createAccount( Account account) { 6 return accountRepository.save(account); 7 } 8 9 10 public Mono<Account> getAccount( String accountNum) { 11 return accountRepository.findByAccountNum(accountNum).switchIfEmpty(Mono.error(new AccountNotFoundException())); 12 } 13 //... 14 }
Este trecho mostra dois endpoints:
- Um endpoint do método POST que cria uma conta
- Um endpoint do método GET que recupera uma conta, mas lança uma exceção se ela não puder ser encontrada
Ambos simplesmente retornam um
Mono<Account>
de AccountRepository.java, uma interfaceReactiveMongoRespository
que atua como uma abstração do Reactive Streams Driversubjacente..save(...)
O método cria um novo documento na coleção de contas em nosso MongoDB database..findByAccountNum()
O método busca um documento que corresponde aoaccountNum
.
1 public interface AccountRepository extends ReactiveMongoRepository<Account, String> { 2 3 4 Mono<Account> findByAccountNum(String accountNum); 5 //... 6 }
A anotação @Query permite especificar uma query do MongoDB com espaços reservados para que ela possa ser substituída dinamicamente por valores de argumentos de método.
?0
seria substituído pelo valor do primeiro argumento de método e ?1
seria substituído pelo segundo, e assim por diante.O mecanismo de construtor de queryembutido pode realmente determinar a query destinada com base no nome do método. Nesse caso, poderíamos excluir a anotação @Query, mas deixei-a lá para maior clareza e para ilustrar o ponto anterior.
Observe que não há necessidade de declarar um método
save(...)
, embora estejamos usando accountRepository.save()
no accountcontroller.java. O métodosave(...)
e muitos outros métodos básicos já estão declarados por interfaces na cadeia de herança de ReactiveMongoRepository
.Esta seção mostra:
- Atualizar operações com
ReactiveMongoRepository
. - Operações de criação, leitura e atualização com
ReactiveMongoTemplate
.
Voltar para
AccountController.java
:1 2 public class AccountController { 3 //... 4 5 public Mono<Txn> debitAccount( String accountNum, Map<String, Object> requestBody) { 6 //... 7 txn.addEntry(new TxnEntry(accountNum, amount)); 8 return txnService.saveTransaction(txn).flatMap(txnService::executeTxn); 9 } 10 11 12 public Mono<Txn> creditAccount( String accountNum, Map<String, Object> requestBody) { 13 //... 14 txn.addEntry(new TxnEntry(accountNum, -amount)); 15 return txnService.saveTransaction(txn).flatMap(txnService::executeTxn); 16 } 17 18 19 public Mono<Txn> transfer( String from, TransferRequest transferRequest) { 20 //... 21 txn.addEntry(new TxnEntry(from, -amount)); 22 txn.addEntry(new TxnEntry(to, amount)); 23 //save pending transaction then execute 24 return txnService.saveTransaction(txn).flatMap(txnService::executeTxn); 25 } 26 //... 27 }
Este snippet mostra três endpoints:
- Um endpoint
.../debit
que adiciona ao saldo de uma conta - Um endpoint
.../credit
que subtrai do saldo de uma conta - Um endpoint
.../transfer
que executa uma transferência de uma conta para outra
Observe que todos os três métodos são muito semelhantes. A ideia principal é:
- Um
Txn
pode consistir em um para muitosTxnEntry
. - Um
TxnEntry
é o reflexo de uma alteração que estamos prestes a fazer em uma única conta. - Um débito ou crédito
Txn
terá apenas umTxnEntry
. - Uma transferência
Txn
terá doisTxnEntry
. - Em todas as três operações, primeiro salvamos um registro do
Txn
que estamos prestes a executar e, em seguida, usamos o TxnService.javapara fazer as alterações desejadas nas contas de destino.
1 2 public class TxnService { 3 //... 4 public Mono<Txn> saveTransaction(Txn txn) { 5 return txnTemplate.save(txn); 6 } 7 8 public Mono<Txn> executeTxn(Txn txn) { 9 return updateBalances(txn) 10 .onErrorResume(DataIntegrityViolationException.class 11 /*lambda expression to handle error*/) 12 .onErrorResume(AccountNotFoundException.class 13 /*lambda expression to handle error*/) 14 .then(txnTemplate.findAndUpdateStatusById(txn.getId(), TxnStatus.SUCCESS)); 15 } 16 17 public Flux<Long> updateBalances(Txn txn) { 18 //read entries to update balances, concatMap maintains the sequence 19 Flux<Long> updatedCounts = Flux.fromIterable(txn.getEntries()).concatMap( 20 entry -> accountRepository.findAndIncrementBalanceByAccountNum(entry.getAccountNum(), entry.getAmount()) 21 ); 22 return updatedCounts.handle(/*...*/); 23 } 24 }
O método
updateBalances(...)
é responsável por iterar cada TxnEntry
e fazer as atualizações correspondentes a cada conta. Isso é feito chamando o métodofindAndIncrementBalanceByAccountNum(...)
em accountRespository.java.1 public interface AccountRepository extends ReactiveMongoRepository<Account, String> { 2 //... 3 4 Mono<Long> findAndIncrementBalanceByAccountNum(String accountNum, double increment); 5 }
Semelhante à declaração de métodos
find
, você também pode declarar métodos de manipulação de dados no ReactiveMongoRepository
, como métodosupdate
. Mais uma vez, o mecanismo do construtor de consultas é capaz de determinar que estamos interessados em consultar por accountNum
com base na nomenclatura do método e definimos a ação de uma atualização usando a anotação@Update
. Nesse caso, a ação é um $inc
e observe que usamos ?1
como espaço reservado porque queremos substituí-lo pelo valor do segundo argumento do método.Seguindo em frente, em
TxnService
também temos:- Um método
saveTransaction
que salva um documentoTxn
na collectiontransactions
. - Um método
executeTxn
que chamaupdateBalances(...)
e atualiza o status da transação no documentoTxn
criado.
Ambos utilizam o
TxnTemplate
que contém um ReactiveMongoTemplate
.1 2 public class TxnTemplate { 3 //... 4 public Mono<Txn> save(Txn txn) { 5 return template.save(txn); 6 } 7 8 public Mono<Txn> findAndUpdateStatusById(String id, TxnStatus status) { 9 Query query = query(where("_id").is(id)); 10 Update update = update("status", status); 11 FindAndModifyOptions options = FindAndModifyOptions.options().returnNew(true); 12 return template.findAndModify(query, update, options, Txn.class); 13 } 14 //... 15 }
O
ReactiveMongoTemplate
nos fornece maneiras mais personalizáveis de interagir com MongoDB e é uma camada mais fina de abstração em comparação com ReactiveMongoRepository
.No método
findAndUpdateStatusById(...)
, estamos definindo praticamente a lógica da query por código, mas também podemos especificar que a atualização deve retornar o documento recém-atualizado.O recurso de transferência neste aplicativo é um caso de uso perfeito para transações com vários documentos, pois as atualizações em duas contas precisam ser atômicas.
Para que o aplicativo obtenha acesso ao suporte de transações do Spring, primeiro precisamos adicionar um
ReactiveMongoTransactionManager
bean à nossa configuração da seguinte forma:1 2 public class ReactiveMongoConfig extends AbstractReactiveMongoConfiguration { 3 //... 4 5 ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) { 6 return new ReactiveMongoTransactionManager(dbFactory); 7 } 8 }
Com isso, podemos definir o escopo de nossas transações. Mostraremos dois métodos:
1. Como usar o TransactionalOperator
O
ReactiveMongoTransactionManager
nos fornece um TransactionOperator
.Podemos então definir o escopo de uma transação anexando
.as(transactionalOperator::transactional)
à chamada do método.1 2 public class TxnService { 3 //In the actual code we are using constructor injection instead of @Autowired 4 //Using @Autowired here to keep code snippet concise 5 6 private TransactionalOperator transactionalOperator; 7 //... 8 public Mono<Txn> executeTxn(Txn txn) { 9 return updateBalances(txn) 10 .onErrorResume(DataIntegrityViolationException.class 11 /*lambda expression to handle error*/) 12 .onErrorResume(AccountNotFoundException.class 13 /*lambda expression to handle error*/) 14 .then(txnTemplate.findAndUpdateStatusById(txn.getId(), TxnStatus.SUCCESS)) 15 .as(transactionalOperator::transactional); 16 } 17 //... 18 }
2. Usando a anotação @Transactional
Também podemos simplesmente definir o escopo de nossa transação anotando o método com a anotação
@Transactional
.1 public class TxnService { 2 //... 3 4 public Mono<Txn> executeTxn(Txn txn) { 5 return updateBalances(txn) 6 .onErrorResume(DataIntegrityViolationException.class 7 /*lambda expression to handle error*/) 8 .onErrorResume(AccountNotFoundException.class 9 /*lambda expression to handle error*/) 10 .then(txnTemplate.findAndUpdateStatusById(txn.getId(), TxnStatus.SUCCESS)); 11 } 12 //... 13 }
Terminamos! Esperemos que esta publicação tenha sido útil para você de uma forma ou de outra. Se você tiver alguma dúvida, visite a MongoDB Community, onde os engenheiros do MongoDB e a comunidade podem ajudá-lo com sua próxima grande ideia!
Mais uma vez, você pode acessar o código a partir do repositório GitHube, se estiver apenas começando, pode valer a pena marcar o Spring Data MongoDBcomo favorito.
Principais comentários nos fóruns
Sai_Kumar_TataSai Kumar Tata2 meses atrás
Conforme documento oficial, o MongoDB tem certas limitações em transações reativas.
Sessões e transações :: Spring Data MongoDB
Como podemos superar isso.