Using MongoDB in Ktor.

Using a database layer in any backend application is important. In this blog, you are going to learn how to use MongoDB as your database.

Subscribe to my newsletter and never miss my upcoming articles

This is the 4th blog in series of Kotlin for Backend. Do check it out if you want to get started in building backend with Kotlin using Ktor.

Till now, while creating an API you used collections in Kotlin to mock the database layer.

In this part, you are going to explore the database layer integration to your project. You are going to use MongoDB as your database for the project.

In this blog, you will understand,

  • What is MongoDB?
  • Integration with your project.
  • Initialize MongoDB in the project.
  • Getting hands dirty in MongoDB
  • Migrating your project with MongoDB

Using a database for any backend project is the core part of building a backend application.

So what are we waiting for, let us get started :)

What is MongoDB?

MongoDB is a document-based database that stores value in the form of JSON. In simpler terms, you can store data in MongoDB using plain text without taking care of Rows/Columns and structure.

Read more about it here

Read more on how to install MongoDB here

Integration with your project.

Now, let us start the integration of MongoDB in your Ktor project. You can use either MongoDB's official Java library or you also have a community-driven library called KMongo. Internally, it uses MongoDB's official Java library which has Coroutines and RxJava support out of the box.

To start using it in the project, you need to add the dependency like,

implementation("org.litote.kmongo:kmongo:4.2.8")

or if you want to use Coroutines version of the library use,

implementation("org.litote.kmongo:kmongo-coroutine:4.2.8")

You will be using the Coroutine version of the library for this blog series.

There are three steps in initializing a database in any project.

DB Client -> Database -> Table/Collection

Let us learn about this in the next section.

Initialize MongoDB in the project.

Let's break it down into steps.

Step 01.

Initialization of KMongo is pretty easy. The easiest way is to use,

val client = KMongo.createClient()

This initializes the MongoDB client from the Java library and creates the instance of the client. But, in our case, you are using coroutines so you are going to initialize the coroutine version of it.

To initialize the coroutine version you can use,

val client = KMongo.createClient().coroutine

Here, coroutine is an extension function for the MongoClient.

This gets the client up and running in your project.

Step 02.

Now, since you have your client setup, you need your database to be configured as well. To configure the database in your project you can use,

val database = client.getDatabase("your_db_name")

This will create a Database with your preferred name. This database will be responsible to hold the collections used by the project.

Step 03.

Finally, you need to create a collection as well.

Collection is like a book that will store the data of a specific type.

If you remember, in the previous blog you created a User class that has id, name, and age. It looks like,

data class User(val id: Int, val name: String, val age: Int)

Here, you will create a collection for the User class like,

val col = database.getCollection<User>()

Using getCollection gets the collection for you with a specific class and returns a CoroutineCollection of User.

CoroutineCollection is a wrapper around MongoCollection in the Java driver APIs.

Now, you are done creating the Mongo client database and the User collection to store your data.

In the next section, you are going to see the basic usage of KMongo so that you can prep yourselves to refactor your code by adding the database layer.

Getting hands dirty in MongoDB

In the basic usage what you are going to do is, you are going to perform few operations in MongoDB using KMongo.

First, let's update your User class like,

data class User(
    @BsonId
    val id: Int,
    val name: String,
    val age: Int
)

BSON stands for Binary JavaScript Object Notation.

Here, BsonId represents the unique id, the document will have in the ObjectId format. This will be unique identifier for your document which will be inserted.

Let us perform create operation.

You have your collection as,

val col = database.getCollection<User>()

Now, to insert/create a new document you can use,

col.insertOne(User("Himanshu", 27))

This will successfully create a User document in the collection.

But let us consider you have multiple users like in a collection and you want to insert a list of users, we can use,

col.insertMany(listOf(User("Himanshu", 27), User("John", 25)))

This is how you can create single or multiple documents using an insert query in MongoDB.

Let us perform the Read operation.

To find a single document from a collection you can use,

col.findOne(User::name eq "Himanshu")

This will return the user object.

To find a single document from a collection using id you can use,

col.findOneById("user_id")

This will return the user object.

And lastly, if you want to get all the users from the collection you can use,

val users = col.find().toList()

This will return all the users in the collection as a list of users.

Let us check an example here:

Consider you want to find all the users who are of 27 years of age in the collection. How will you find it?

Hint: Use find()

Solution:

Type 1: Inefficient solution

You can use the find method to get all the documents and then use the Kotlin find operator to get the desired result like,

val users = col.find().toList().find { 
                it.age == 27
            }

This is inefficient because in any case, it is first scanning the whole collection and then filtering out the data.

Type 2: Efficient solution

val list : List<User> = col.find(User::age eq 27).toList()

This will return all the users from the collection of 27years of age. eq is an operator of MongoDB which means equals.

Here, it scanned the collection to find all the documents with age == 27.

Similarly, you have methods like, updateOne(), updateOneById(), deleteOne() etc.

Migrating your project with MongoDB

As per the previous part of the series, you have the following code with multiple routes working with a list.

Not considering the location for this blog

fun Application.configureRouting() {
    install(ContentNegotiation) {
        gson()
    }

    routing {

        get("/users") {
            call.respond(users)
        }

        post("/user") {
            val requestBody = call.receive<User>()
            val user = requestBody.copy(id = users.size.plus(1))
            users.add(user)
            call.respond(user)
        }

        get("/user") {
            val id = call.request.queryParameters["id"]
            val user = users.find { it.id == id?.toInt() }
            val response = user ?: "User not found"
            call.respond(response)
        }

        delete("/user") {
            val id = call.request.queryParameters["id"]
            users.removeIf {
                it.id == id?.toInt()
            }
            call.respond("User deleted")
        }
    }
}

And here, the User class and users list looks like,

data class User(val id: Int, val name: String, val age: Int)

val users = mutableListOf<User>()

As a next step, you are going to migrate from using users list to Mongo operations.

Let us break it down into steps.

Step 01.

In the application file, inside the main function, you will set up the Mongo client, DB, and collection.

  val client = KMongo.createClient().coroutine
  val database = client.getDatabase("users")
  val col = database.getCollection<User>()

Here, you have your client, database, and col(which is a collection).

And you will pass this collection in configure routing function as a parameter. Now, the main() function looks like,

fun main() {
    val client = KMongo.createClient().coroutine
    val database = client.getDatabase("users")
    val col = database.getCollection<User>()

    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting(col)
    }.start(wait = true)
}

Now in the next step, you will use this collection.

Step 02.

Here, the updated code with the collection as parameter looks like,

fun Application.configureRouting(collection: CoroutineCollection<User>) {
    install(ContentNegotiation) {
        gson()
    }
   // your code
}

Let us now update the code one by one.

Inside the routing you have the first route as,

 get("/users") {
    call.respond(users)
 }

This returns all the users. To replicate this in Mongo you will use,

 get("/users") {
    val users = collection.find().toList()
    call.respond(users)
 }

This will now return all the users from the User collection.

Now, onto the next route. You have here,

post("/user") {
        call.parameters
        val requestBody = call.receive<User>()
        val user = requestBody.copy(id = users.size.plus(1))
        users.add(user)
        call.respond(user)
}

This route inserts a new user. Here, you are manually updating the id of the user. But if you can remember that we updated your User class like,

data class User(
    @BsonId
    val id: Int,
    val name: String,
    val age: Int
)

Here, the annotation @BsonId will take care of the id generation in the document. So, the insert the request into the collection you will update it as,

post("/user") {
    call.parameters
    val requestBody = call.receive<User>()
    val isSuccess = collection.insertOne(requestBody).wasAcknowledged()
    call.respond(isSuccess)
}

You can see, insertOne is used to insert the user in the collection. Here, we are using wasAcknowledged because if the write is successful we will return true or else false.

Similarly, for the next two routes, you can update the code with the Mongo operators like findOne and deleteOne.

This is all you need to know to get started in using MongoDB in a Ktor project.

If you like the series, do share it with people :)

Check the real implementations of Ktor with MongoDB here.

Thank you for reading :) Hope you learned something from my experience.

If you have anything more to discuss, I will be happy to get connected at hi_man_shoe.

Shreyas Patil's photo

Good read

Raghav Awasthi's photo

Damnnn Awesome 😎

Runner-Shane's photo

The blog is awesome but when I try to run the app then I am getting the following exception

2021-10-14 11:22:37.244 [cluster-ClusterId{value='6167c5a470b9a401ce681185', description='null'}-localhost:27017] INFO org.mongodb.driver.cluster - Exception in monitor thread while connecting to server localhost:27017 com.mongodb.MongoSocketOpenException: Exception opening socket at com.mongodb.internal.connection.AsynchronousSocketChannelStream$OpenCompletionHandler.failed(AsynchronousSocketChannelStream.java:124) at java.base/sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:129) at java.base/sun.nio.ch.Invoker$2.run(Invoker.java:219) at java.base/sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829) Caused by: java.net.ConnectException: Connection refused at java.base/sun.nio.ch.UnixAsynchronousSocketChannelImp..(Native Method) at java.base/sun.nio.ch.UnixAsynchronousSocketChannelImp..(UnixAsynchronousSocketChannelImpl.java:252) at java.base/sun.nio.ch.UnixAsynchronousSocketChannelImp..(UnixAsynchronousSocketChannelImpl.java:198) at java.base/sun.nio.ch.UnixAsynchronousSocketChannelImp..(UnixAsynchronousSocketChannelImpl.java:213) at java.base/sun.nio.ch.EPollPort$EventHandlerTask.run(EPollPort.java:306) ... 1 common frames omitted

so can you help me solve this