Introdução ao MongoDB Kotlin Driver
Avalie esse Tutorial
Este é um artigo introdutório sobre como criar um aplicativo no Kotlin usando o MongoDB Atlas e o driver do MongoDB Kotlin, a adição mais recente à nossa lista de drivers oficiais. Juntos, criaremos um aplicativo CRUD que abrange os fundamentos de como usar o MongoDB como banco de dados, enquanto aproveitamos os benefícios do Kotlin como linguagem de programação, como classes de dados, corrotinas e fluxo.
Este é um artigo de introdução. Portanto, não é necessário muito como pré-requisito, mas a familiaridade com o Kotlin como linguagem de programação será útil.
Além disso, precisamos de uma conta do Atlas, que é gratuita para sempre. Crie uma conta, caso ainda não tenha. Isso fornece o MongoDB como um banco de dados em nuvem e muito mais. Mais adiante neste tutorial, usaremos essa conta para criar um novo cluster, carregar um conjunto de dados e, por fim, consultá-lo.
Em geral, o MongoDB é um banco de dados de documentos distribuído e multiplataforma que permite criar aplicativos com esquema flexível. Caso você não esteja familiarizado com ele ou queira fazer uma rápida recapitulação, recomendamos explorar a série MongoDB Jumpstart para se familiarizar com o MongoDB e seus vários serviços em menos 10 minutos. Ou, se preferir ler, siga nosso guia.
E, por último, para ajudar em nossas atividades de desenvolvimento, usaremos o Jetbrains IntelliJ IDEA (Community Edition), que tem suporte padrão para a linguagem Kotlin.
Antes de começarmos, gostaria de falar um pouco sobre o Realm Kotlin SDK, um dos SDKs usados para criar aplicações móveis do lado do cliente usando o ecossistema MongoDB. Ele não deve ser confundido com o driver MongoDB Kotlin para programação do lado do servidor. O driver MongoDB Kotlin, um driver de linguagem, permite que você interaja perfeitamente com o Atlas, um banco de dados em nuvem, com os benefícios do paradigma da linguagem Kotlin. É apropriado para criar aplicações de back-end, scripts, etc.
Para tornar o aprendizado mais significativo e prático, criaremos um aplicativo CRUD. Confira nosso repositório do Github se quiser acompanhar. Então, vamos começar.
Para criar o projeto, podemos usar o assistente de projeto, que pode ser encontrado nas opções de menu
File
. Em seguida, selecione New
, seguido de Project
. Isso abrirá a tela New Project
, conforme mostrado abaixo, e atualizará o projeto e a linguagem para Kotlin.Após a sincronização inicial do Gradle, nosso projeto está pronto para ser executado. Então, vamos tentar usar o ícone de execução na barra de menu ou simplesmente pressionar CRTL + R no Mac. Atualmente, nosso projeto não faz muito além de imprimir
Hello World!
e os argumentos fornecidos, mas a mensagem BUILD SUCCESSFUL
no console de execução é o que estamos procurando, o que nos informa que a configuração do projeto está concluída.Agora, a próxima etapa é adicionar o driver Kotlin ao nosso projeto, o que nos permite interagir com o MongoDB Atlas.
Adicionar o driver ao projeto é simples e direto. Basta atualizar o bloco
dependencies
com a dependência do driver Kotlin no arquivo de criação — ou seja, build.gradle
.1 dependencies { 2 // Kotlin coroutine dependency 3 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") 4 5 // MongoDB Kotlin driver dependency 6 implementation("org.mongodb:mongodb-driver-kotlin-coroutine:4.10.1") 7 }
Para se conectar ao banco de dados, primeiro precisamos do
Connection URI
que pode ser encontrado pressionando connect to cluster
em nossa conta Atlas, conforme mostrado abaixo.Com o URI de conexão disponível, a próxima etapa é criar um arquivo Kotlin.
Setup.kt
é onde gravamos o código para conexão com o MongoDB Atlas.A conexão com o nosso banco de dados pode ser feita em duas etapas. Primeiro, criamos uma instância do MongoClient usando
Connection URI
.1 val connectionString = "mongodb+srv://<username>:<enter your password>@cluster0.sq3aiau.mongodb.net/?retryWrites=true&w=majority" 2 val client = MongoClient.create(connectionString = connectString)
Depois, use o cliente para se conectar ao banco de dados,
sample_restaurants
, um conjunto de dados de amostra para restaurantes. Um conjunto de dados de amostra é uma ótima maneira de explorar a plataforma e criar um POC mais realista para validar suas ideias. Para saber como propagar seu primeiro banco de dados Atlas com dados de amostra, visite a documentação.1 val databaseName = "sample_restaurants" 2 val db: MongoDatabase = client.getDatabase(databaseName = databaseName)
A codificação
connectionString
não é uma boa abordagem e pode levar a riscos de segurança ou à incapacidade de fornecer acesso baseado em função. Para evitar esses problemas e seguir as melhores práticas, usaremos variáveis de ambiente. Outras abordagens comuns são o uso do Vault, variáveis de configuração de compilação e variáveis de ambiente de CI/CD.Para adicionar variáveis de ambiente, use
Modify run configuration
, que pode ser encontrado clicando com o botão direito do mouse no arquivo.Juntamente com o código para acessar a variável de ambiente, nosso código final tem a seguinte aparência.
1 suspend fun setupConnection( 2 databaseName: String = "sample_restaurants", 3 connectionEnvVariable: String = "MONGODB_URI" 4 ): MongoDatabase? { 5 val connectString = if (System.getenv(connectionEnvVariable) != null) { 6 System.getenv(connectionEnvVariable) 7 } else { 8 "mongodb+srv://<usename>:<password>@cluster0.sq3aiau.mongodb.net/?retryWrites=true&w=majority" 9 } 10 11 val client = MongoClient.create(connectionString = connectString) 12 val database = client.getDatabase(databaseName = databaseName) 13 14 return try { 15 // Send a ping to confirm a successful connection 16 val command = Document("ping", BsonInt64(1)) 17 database.runCommand(command) 18 println("Pinged your deployment. You successfully connected to MongoDB!") 19 database 20 } catch (me: MongoException) { 21 System.err.println(me) 22 null 23 } 24 }
No trecho de código acima, ainda podemos usar uma string codificada. Isso é feito apenas para fins de demonstração, permitindo que você use um URI de conexão diretamente para facilitar e executar isso por meio de qualquer editor on-line. Mas é altamente recomendável evitar a codificação de um URI de conexão.
Com a função
setupConnection
pronta, vamos testá-la e realizar a query do banco de dados para obter a contagem e o nome da coleção.1 suspend fun listAllCollection(database: MongoDatabase) { 2 3 val count = database.listCollectionNames().count() 4 println("Collection count $count") 5 6 print("Collection in this database are -----------> ") 7 database.listCollectionNames().collect { print(" $it") } 8 }
Ao executar esse código, nosso resultado fica assim:
Até agora, você deve ter notado que estamos usando a palavra-chave
suspend
com listAllCollection()
. listCollectionNames()
é uma função assíncrona, pois interage com o banco de dados e, portanto, idealmente seria executada em uma thread diferente. E como o driver Kotlin do MongoDB oferece suporte ao Coroutines, o paradigma de linguagem assíncrona, nativo do Kotlin, podemos nos beneficiar dele usando funções suspend
.Da mesma forma, para descartar coleções, usamos a função
suspend
.1 suspend fun dropCollection(database: MongoDatabase) { 2 database.getCollection<Objects>(collectionName = "restaurants").drop() 3 }
Com isso concluído, estamos prontos para começar a trabalhar em nosso aplicativo CRUD. Portanto, para começar, precisamos criar uma classe
data
que represente as informações do restaurante que nosso aplicativo salva no banco de dados.1 data class Restaurant( 2 3 val id: ObjectId, 4 val address: Address, 5 val borough: String, 6 val cuisine: String, 7 val grades: List<Grade>, 8 val name: String, 9 10 val restaurantId: String 11 ) 12 13 data class Address( 14 val building: String, 15 val street: String, 16 val zipcode: String, 17 val coord: List<Double> 18 ) 19 20 data class Grade( 21 val date: LocalDateTime, 22 val grade: String, 23 val score: Int 24 )
No trecho de código acima, usamos duas anotações:
@BsonId
, que representa a identidade exclusiva ou_id
de um documento.@BsonProperty
, que cria um alias para chaves no documento – por exemplo,restaurantId
representarestaurant_id
.
Observação: nossa classe
Restaurant
aqui é uma réplica exata de um documento de restaurante no conjunto de dados de exemplo, mas alguns campos podem ser ignorados ou marcados como opcionais - por exemplo, grades
e address
- enquanto mantém a capacidade de executar operações CRUD. Podemos fazer isso, pois o document model do MongoDB permite um esquema flexível para nossos dados.Com todo o trabalho pesado feito (10 linhas de código para conexão), adicionar um novo documento ao banco de dados é realmente simples e pode ser feito com uma linha de código usando
insertOne
. Então, vamos criar um novo arquivo chamado Create.kt
, que conterá todas as operações de criação.1 suspend fun addItem(database: MongoDatabase) { 2 3 val collection = database.getCollection<Restaurant>(collectionName = "restaurants") 4 val item = Restaurant( 5 id = ObjectId(), 6 address = Address( 7 building = "Building", street = "street", zipcode = "zipcode", coord = 8 listOf(Random.nextDouble(), Random.nextDouble()) 9 ), 10 borough = "borough", 11 cuisine = "cuisine", 12 grades = listOf( 13 Grade( 14 date = LocalDateTime.now(), 15 grade = "A", 16 score = Random.nextInt() 17 ) 18 ), 19 name = "name", 20 restaurantId = "restaurantId" 21 ) 22 23 collection.insertOne(item).also { 24 println("Item added with id - ${it.insertedId}") 25 } 26 }
Quando o executamos, a saída no console é:
Reitero, não se esqueça de adicionar uma variável de ambiente novamente para este arquivo, se você teve problemas ao executá-lo.
Se quisermos adicionar vários documento à coleção, podemos usar
insertMany
, que é recomendado em vez da execução de insertOne
em um loop.1 suspend fun addItems(database: MongoDatabase) { 2 val collection = database.getCollection<Restaurant>(collectionName = "restaurants") 3 val newRestaurants = collection.find<Restaurant>().first().run { 4 listOf( 5 this.copy( 6 id = ObjectId(), name = "Insert Many Restaurant first", restaurantId = Random 7 .nextInt().toString() 8 ), 9 this.copy( 10 id = ObjectId(), name = "Insert Many Restaurant second", restaurantId = Random 11 .nextInt().toString() 12 ) 13 ) 14 } 15 16 collection.insertMany(newRestaurants).also { 17 println("Total items added ${it.insertedIds.size}") 18 } 19 }
Com esses resultados no console, podemos dizer que os dados foram adicionados com sucesso.
Mas e se quisermos ver o objeto no banco de dados? Uma maneira é com uma operação de leitura, que faríamos em breve ou usaríamos o MongoDB Compass para visualizar as informações.
O MongoDB Compass é uma ferramenta GUI interativa e gratuita para consultar, otimizar e analisar os dados do MongoDB em seu sistema. Para começar, baixe a ferramenta e use
connectionString
para se conectar ao banco de dados.Para ler as informações do banco de dados, podemos usar o operador
find
. Vamos começar lendo qualquer documento.1 val collection = database.getCollection<Restaurant>(collectionName = "restaurants") 2 collection.find<Restaurant>().limit(1).collect { 3 println(it) 4 }
O operador
find
retorna uma lista de resultados, mas como estamos interessados apenas em um único documento, podemos usar o operador limit
para limitar nosso conjunto de resultados. Nesse caso, seria um único documento.Se estendermos isso ainda mais e quisermos ler um documento específico, poderemos adicionar parâmetros de filtro sobre ele:
1 val queryParams = Filters 2 .and( 3 listOf( 4 eq("cuisine", "American"), 5 eq("borough", "Queens") 6 ) 7 )
1 suspend fun readSpecificDocument(database: MongoDatabase) { 2 val collection = database.getCollection<Restaurant>(collectionName = "restaurants") 3 val queryParams = Filters 4 .and( 5 listOf( 6 eq("cuisine", "American"), 7 eq("borough", "Queens") 8 ) 9 ) 10 11 12 collection 13 .find<Restaurant>(queryParams) 14 .limit(2) 15 .collect { 16 println(it) 17 } 18 19 }
Para a saída, vemos isso:
Não se esqueça de adicionar a variável de ambiente novamente para este arquivo, se você teve problemas ao executá-lo.
Outro caso de uso prático que vem com uma operação de leitura é a paginação aos resultados. Isso pode ser feito com os operadores
limit
e offset
.1 suspend fun readWithPaging(database: MongoDatabase, offset: Int, pageSize: Int) { 2 val collection = database.getCollection<Restaurant>(collectionName = "restaurants") 3 val queryParams = Filters 4 .and( 5 listOf( 6 eq(Restaurant::cuisine.name, "American"), 7 eq(Restaurant::borough.name, "Queens") 8 ) 9 ) 10 11 collection 12 .find<Restaurant>(queryParams) 13 .limit(pageSize) 14 .skip(offset) 15 .collect { 16 println(it) 17 } 18 }
Mas com essa abordagem, frequentemente, o tempo de resposta da query aumenta com o valor do
offset
. Para superar isso, podemos nos beneficiar criando um Index
, como mostrado abaixo.1 val collection = database.getCollection<Restaurant>(collectionName = "restaurants") 2 val options = IndexOptions().apply { 3 this.name("restaurant_id_index") 4 this.background(true) 5 } 6 7 collection.createIndex( 8 keys = Indexes.ascending("restaurant_id"), 9 options = options 10 )
Agora, vamos discutir como editar/atualizar um documento existente. Novamente, vamos criar rapidamente um novo arquivo Kotlin,
Update.Kt
.Em geral, há duas maneiras de atualizar qualquer documento:
- Execute uma operação update, o que nos permite atualizar campos específicos dos documentos correspondentes sem afetar os outros campos.
- Execute uma operação replace para substituir o documento correspondente pelo novo documento.
Para este exercício, usaremos o documento que criamos anteriormente com a operação create
{restaurant_id: "restaurantId"}
e atualizaremos restaurant_id
com um valor mais realista. Vamos dividir isso em duas subtarefas para maior clareza.Primeiro, usando
Filters
, executamos uma query para filtrar o documento, semelhante à operação de leitura anterior.1 val collection = db.getCollection<Restaurant>("restaurants") 2 val queryParam = Filters.eq("restaurant_id", "restaurantId")
Então, podemos definir o
restaurant_id
com um valor inteiro aleatório usando Updates
.1 val updateParams = Updates.set("restaurant_id", Random.nextInt().toString())
E, finalmente, usamos
updateOne
para atualizar o documento em uma operação atômica.1 collection.updateOne(filter = queryParam, update = updateParams).also { 2 println("Total docs modified ${it.matchedCount} and fields modified ${it.modifiedCount}") 3 }
No exemplo acima, já sabíamos qual documento queríamos atualizar – o restaurante com id
restauratantId
– mas poderia haver casos de uso em que essa não seria a situação. Nesses casos, primeiro procuraríamos o documento e depois o atualizaríamos. findOneAndUpdate
pode ser útil. Ele permite combinar esses dois processos em uma operação atômica, desbloqueando desempenho adicional.Outra variação do mesmo pode ser atualizar vários documentos com uma chamada.
updateMany
é útil para esses casos de uso - por exemplo, se quisermos atualizar o cuisine
de todos os restaurantes para seu tipo favorito de culinária e borough
para o Brooklyn.1 suspend fun updateMultipleDocuments(db: MongoDatabase) { 2 val collection = db.getCollection<Restaurant>("restaurants") 3 val queryParam = Filters.eq(Restaurant::cuisine.name, "Chinese") 4 val updateParams = Updates.combine( 5 Updates.set(Restaurant::cuisine.name, "Indian"), 6 Updates.set(Restaurant::borough.name, "Brooklyn") 7 ) 8 9 collection.updateMany(filter = queryParam, update = updateParams).also { 10 println("Total docs matched ${it.matchedCount} and modified ${it.modifiedCount}") 11 } 12 }
Nestes exemplos, usamos
set
e combine
com Updates
. Mas há muitos outros tipos de operador de atualização a serem analisados que nos permitem realizar muitas operações intuitivas, como definir currentDate ou o registro de data e hora, aumentar ou diminuir o valor do campo e assim por diante. Para saber mais sobre os diferentes tipos de operadores de atualização que você pode executar com Kotlin e MongoDB, consulte nossos documentos.Agora, vamos explorar uma última operação CRUD: excluir. Começaremos explorando como excluir um único documento. Para fazer isso, usaremos
findOneAndDelete
em vez de deleteOne
. Como benefício adicional, isso também retorna o documento excluído como saída. Em nosso exemplo, excluímos o restaurante:1 val collection = db.getCollection<Restaurant>(collectionName = "restaurants") 2 val queryParams = Filters.eq("restaurant_id", "restaurantId") 3 4 collection.findOneAndDelete(filter = queryParams).also { 5 it?.let { 6 println(it) 7 } 8 }
Para excluir vários documentos, podemos usar
deleteMany
. Podemos, por exemplo, usar isso para excluir todos os dados que criamos anteriormente com nossa operação de criação.1 suspend fun deleteRestaurants(db: MongoDatabase) { 2 val collection = db.getCollection<Restaurant>(collectionName = "restaurants") 3 4 val queryParams = Filters.or( 5 listOf( 6 Filters.regex(Restaurant::name.name, Pattern.compile("^Insert")), 7 Filters.regex("restaurant_id", Pattern.compile("^restaurant")) 8 ) 9 ) 10 collection.deleteMany(filter = queryParams).also { 11 println("Document deleted : ${it.deletedCount}") 12 } 13 }
Parabéns! Agora você sabe configurar seu primeiro aplicativo Kotlin com MongoDB e executar operações CRUD. O código-fonte completo do aplicativo pode ser encontrado no GitHub.
Se você tiver algum feedback sobre sua experiência de trabalho com o driver MongoDB Kotlin, envie um comentário em nosso portal de feedback do usuário ou entre em contato conosco no Twitter: @codeWithMohit.