Dados da primavera desbloqueados: queries avançadas com MongoDB
Ricardo Mello7 min read • Published Nov 08, 2024 • Updated Nov 08, 2024
APLICATIVO COMPLETO
Avalie esse Tutorial
Aposto que você está aqui porque já leu o primeiro artigo desta série, Começando com Java e MongoDB. Mas, mesmo que não tenha feito isso, não se incomode — você está no lugar certo. Na primeira parte desta série, exploramos os principais conceitos relacionados ao Spring e aprendermos como criar um projeto e configurá-lo para integração perfeita com o MongoDB. Esta é a segunda parte da série Spring Data Unlocked, em que continuaremos trabalhando em nosso projeto e exploraremos os recursos do Spring Data criando queries avançadas e vendo como isso pode ser feito sem problemas.
- Comece a usar o MongoDB Atlas gratuitamente! Se você ainda não tiver uma conta, o MongoDB oferece um cluster Atlas gratuito para sempre.
- IDE de sua escolha
Se você não se lembra do que conversamos antes, não se desespere. Leia o artigo anterior sobre como começar a usar Java e MongoDB ou atualize sua memória com nosso modelo de dados no qual estamos trabalhando:
1 { 2 "id": "672182814338f60133ee26e1", 3 "transactionType": "Debit", 4 "amount": 888.0, 5 "currency": "USD", 6 "status": "In Progress", 7 "description": "Transfer to Ricardo", 8 "createdAt": "2024-10-09T14:00:00", 9 "accountDetails": { 10 "originator": { 11 "accountNumber": "2376543213", 12 "name": "Maria", 13 "bank": "Bank G" 14 }, 15 "beneficiary": { 16 "accountNumber": "2234987651", 17 "name": "Ricardo Mello", 18 "bank": "Bank V" 19 } 20 } 21 }
Nosso modelo de transação contém essa estrutura e prosseguiremos com ela.
Ainda com foco na produtividade e na velocidade, continuaremos usando nossa
MongoRepository
interface e criaremos uma query para recuperar nossas transações por tipo. Abra a TransactionRepository
classe e adicione o seguinte método:1 List<Transaction> findByTransactionType(String type);
Como você pode ver, estamos encontrando todas as transações por tipo. O Spring, por meio de convenções de nomenclatura (queries derivadas), permite criar queries com base nos nomes dos métodos.
Dica: podemos monitorar o processo de registro ativando uma configuração DEBUG no
application.properties
arquivo:1 logging.level.org.springframework.data.mongodb=DEBUG
Poderemos ver a query no console:
1 2024-10-15T18:30:33.855-03:00 DEBUG 28992 2 [SpringShop] [nio-8080-exec-6] o.s.data.mongodb.core.MongoTemplate: 3 find using query: { "transactionType" : "Transfer"} fields: Document{{}} sort: { "transactionType" : "Transfer"} for class: class com.mongodb.Transaction in collection: transactions
Agora, digamos que queremos todas as transações cujo valor seja maior que 3000:
1 List<Transaction> findByAmountGreaterThan(double amount);
É possível excluir registros da mesma maneira. Dê uma olhada:
1 void deleteByTransactionType(String type);
Se tudo estiver funcionando corretamente, seu código deverá ser semelhante a este:
1 package com.mongodb; 2 import org.springframework.data.mongodb.repository.MongoRepository; 3 import org.springframework.stereotype.Repository; 4 import java.util.List; 5 6 7 public interface TransactionRepository extends MongoRepository<Transaction, String>{ 8 List<Transaction> findByTransactionType(String type); 9 List<Transaction> findByAmountGreaterThan(double amount); 10 void deleteByTransactionType(String type); 11 }
Continuando nosso desenvolvimento, podemos usar a classeQuery que o Spring fornece em vez de trabalhar com consultas derivadas. Esta alternativa tambémé interessante. Abaixo está um exemplo de como buscar transações por status, exibindo ( Projeção) apenas os campos
createdAt
accountDetail
amount
, e e classificando por createdAt
em ordem decrescente:1 2 3 4 5 6 List<Transaction> findByStatus(String status);
Também podemos executar operações de atualização em nosso banco de dados de dados usando esta anotação. Na primeira parte, aplicamos o filtro e, em seguida, atualizamos o status.
1 2 3 void updateStatus(String id, String status);
Outra anotação importante nos dados da Spring é @Aggregation. Como o nome sugere, permite-nos criar estágios de agregação . Vamos imaginar o seguinte cenário:
Queremos retornar o total de
amount
agrupado por tipo de transação. Para fazer isso, criaremos o seguinte método:1 2 3 4 5 6 List<Transaction> getTotalAmountByTransactionType(String transactionType);
- No primeiro estágio,
$match
, filtramos as transações pelo tipo especificado. - No segundo estágio,,
$group
agrupamos as transações portransactionType
e somamos os valores doamount
campo. - Finalmente, no
$project
estágio, exibimos o total deamount
.
Uma nova solicitação de nossa equipe de produto chegou e eles querem que salvemos todas as transações com um status de erro em uma nova coleção. Podemos definir uma tarefa para ser executada uma vez por dia e acionar trigger um método para lidar com isso para nós. Para isso, podemos usar
$out
o estágio do MongoDB:1 2 3 4 5 6 void exportErrorTransactions();
Depois que esse método for executado, a agregação será processada e quaisquer documentos com um status de erro serão inseridos em uma nova coleção dentro do mesmo banco de dados de dados chamado
error_transactions
.Vamos decompor novamente:
$match
para filtrar transações com status de erro$project
para especificar quais campos queremos incluir na collection de erros$out
para definir a coleção para onde exportaremos as transações de erro
Observação: embora Go não entraremos em detalhes sobre as especificidades de
$out
, é altamente recomendável revisar seu comportamento antes de usá-lo em um ambiente de produção.Como você pode ver, essa anotação é muito avançada e pode fornecer funcionalidades para queries cada vez mais complexas. Vamos presumir que agora temos uma nova collection chamada
customers
com alguns campos adicionais:1 { 2 "name": "Ricardo Mello", 3 "email": "ricardo.mello@mongodb.com", 4 "accountNumber": "2234987651", 5 "phone": "5517992384610", 6 "address": { 7 "street": "Street 001", 8 "city": "New York" 9 } 10 }
Podemos usar
$lookup
para realizar uma união entre as transaction
customer
collections e usando o accountNumber
campo e trazer valores extras como phone
e address
.1 2 3 4 5 6 7 8 9 10 11 12 13 List<CustomerOriginatorDetail> findTransactionsWithCustomerDetails();
No código acima, estamos usando o
$lookup
operador para unir a nova customer
coleção por meio do accountNumber
campo e retornando uma nova lista de CustomerOriginatorDetail
.1 public record CustomerOriginatorDetail( 2 double amount, 3 String status, 4 Transaction.AccountDetails accountDetails, 5 List<OriginatorCustomerDetails> originatorCustomerDetails 6 ) { 7 private record OriginatorCustomerDetails( 8 String name, 9 String accountNumber, 10 String phone, 11 Address address) { 12 13 private record Address(String city, String street) {} 14 } 15 }
Observação: lembre-se de que não estamos nos concentrando em modelagem de dados aqui, apenas mostrando que é possível trabalhar com $lookup nesse contexto.
Outra abordagem com a
@Aggregation
anotação é criar uma interface que represente uma agregação, o que simplifica seu uso. Nossa SearchAggregate
anotação do permite a criação de funcionalidade de pesquisa reutilizável usando a estrutura de agregação do MongoDB, facilitando a consulta eficiente e estruturada de dados de texto em seu banco de dados de dados.1 import org.springframework.data.mongodb.repository.Aggregation; 2 3 import java.lang.annotation.ElementType; 4 import java.lang.annotation.Retention; 5 import java.lang.annotation.RetentionPolicy; 6 import java.lang.annotation.Target; 7 8 9 10 11 12 13 SearchAggregate {14 }
Agora, para utilizar nossa anotação em um método, podemos passar os argumentos para a query e o caminho na
TransactionRepository
classe :1 2 List<Transaction> search(String query, String path);
Um aspecto importante a ser observado é que, se procurarmos as interfaces estendidas por
MongoRepository
, encontraremos PagingAndSortingRepository
, que inclui o método:Page<T> findAll(Pageable pageable);
Este método é crucial para trabalhar com paginação. Vamos Go voltar ao nosso
TransactionService
e implementar o seguinte código:1 public Page<Transaction> findPageableTransactions( 2 Pageable pageable 3 ) { 4 return transactionRepository.findAll(pageable); 5 }
Agora, no TransactionController, podemos fazer a chamada paginada da seguinte maneira:
1 2 public PagedModel<Transaction> findAll( int page, 3 int sizePerPage, 4 String sortField, 5 Sort.Direction sortDirection) { 6 Pageable pageable = PageRequest.of(page, sizePerPage, Sort.by(sortDirection, sortField)); 7 return new PagedModel<>(transactionService.findPageableTransactions(pageable)); 8 }
E, finalmente, podemos chamar nosso método usando o seguinte
curl
comando.1 curl --location 'http://localhost:8080/transactions?page=0&sizePerPage=10&sortField=description&sortDirection=ASC'
Uma alternativa muito interessante para obter flexibilidade e controle sobre a operação do MongoDB é o MongoTemplate. Esse modelo oferece suporte a operações como atualização, inserção e seleção, e também fornece uma interação avançada com o MongoDB, permitindo mapear nossos objetos de domínio diretamente para o modelo document model de documento no banco de banco de dados. Vamos começar criando uma nova
MongoConfig
classe :1 package com.mongodb; 2 3 import com.mongodb.client.MongoClient; 4 import com.mongodb.client.MongoClients; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.data.mongodb.core.MongoOperations; 8 import org.springframework.data.mongodb.core.MongoTemplate; 9 10 11 public class MongoConfig { 12 13 14 public MongoClient mongoClient() { 15 MongoClientSettings settings = MongoClientSettings.builder() 16 .applyConnectionString(new ConnectionString("<your connection string>")) 17 .build(); 18 return MongoClients.create(settings); 19 } 20 21 22 MongoOperations mongoTemplate(MongoClient mongoClient) { 23 return new MongoTemplate(mongoClient, "springshop"); 24 } 25 }
Podemos observar a anotação @Bean da Spring, o que nos permite trabalhar com o MongoTemplate injetado em nossos serviços. Continuando com nosso desenvolvimento, trabalharemos com a classe Cliente para manipulá-la. Para fazer isso, crie o
Customer
registro:1 package com.mongodb; 2 3 public record Customer( 4 String name, 5 String email, 6 String accountNumber, 7 String phone, 8 Address address 9 ) { 10 public record Address( 11 String street, 12 String city 13 ) {} 14 }
Vamos começar com o modelo básico de inserção e depois desenvolver para outras operações. Para fazer isso, criaremos nosso
CustomerService
com o seguinte método:1 import org.springframework.data.mongodb.core.MongoOperations; 2 import org.springframework.stereotype.Service; 3 4 5 public class CustomerService { 6 7 private final MongoOperations mongoOperations; 8 9 CustomerService(MongoOperations mongoOperations) { 10 this.mongoOperations = mongoOperations; 11 } 12 13 public Customer newCustomer() { 14 var customer = new Customer( 15 "Ricardo", 16 "ricardohsmello@gmail.com", 17 "123", 18 "1234", 19 new Customer.Address("a", "Sp") 20 ); 21 22 return mongoOperations.insert(customer); 23 } 24 25 }
Nosso serviço funcionará com o mongoOperations, que lidará com a inserção do nosso documento.
Observe que estou criando (novo cliente) para ilustrar a inserção. Uma boa prática seria receber este cliente como um argumento na nossa função.
Uma maneira de lidar com inserções em massa é usando o BulkWrite. O MongoTemplate nos fornece o método bulkOps, onde podemos fornecer uma lista, e ele será executado em lotes:
1 public int bulkCustomerSample(List<Customer> customerList) { 2 BulkWriteResult result mongoOperations.bulkOps(BulkOperations.BulkMode.ORDERED, Customer.class) 3 .insert(customerList) 4 .execute(); 5 6 return result.getInsertedCount(); 7 }
Passando para queries, temos a implementação de queries na classe MongoTemplate . No código abaixo, estamos procurando um cliente por e-mail e retornando apenas um:
1 public Customer findCustomerByEmail(String email) { 2 return mongoOperations.query(Customer.class) 3 .matching(query(where("email").is(email))) 4 .one() 5 .orElseThrow(() -> new RuntimeException("Customer not found with email: " + email)); 6 }
O método de query (importado estaticamente) espera um Critério (onde), também importado estaticamente, onde podemos aplicar vários filtros, como gt(), lt() e() e ou(). Para obter referência, consulte a documentação da Spring.
Vamos supor que precisamos de uma query que retorne o número total de clientes por cidade. Para isso, precisamos agrupar por cidade e contar o número de clientes. Para atingir esse objetivo, primeiro, vamos criar um novo registro para lidar com isso:
1 public record CustomersByCity( 2 String id, 3 int total 4 ){}
Em seguida, criaremos um método totalCustomerByCity:
1 public List<CustomersByCity> totalCustomerByCity() { 2 3 TypedAggregation<Customer> aggregation = newAggregation(Customer.class, 4 group("address.city") 5 .count().as("total"), 6 Aggregation.sort(Sort.Direction.ASC, "_id"), 7 project(Fields.fields("total", "_id"))); 8 9 AggregationResults<CustomersByCity> result = mongoOperations.aggregate(aggregation, CustomersByCity.class); 10 return result.getMappedResults(); 11 }
O método estático newAggregation nos oferece uma boa abordagem para manipular nossos estágios de agregação . Estamos agrupando por cidade e usando a função count(), que realiza internamente uma soma sobre o número total de clientes, classificando por _id _id em ordem crescente e exibindo os ID campos total e ID.
Nesta segunda parte de nossa série, Spring Data Unlocked, exploramos como criar queries complexas com MongoRepository e MongoTemplate. Este tutorial abordou conceitos como paginação, anotações personalizadas, inserções em massa e consultas de agregação avançadas.
Se você tiver alguma dúvida, fique à vontade para deixá-la nos comentários.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.