Connecting Micronaut to MongoDB database using Kotlin
Tags: kotlin, mongodb, micronaut
Published: 2023-02-21 16:40:59

MongoDB is really flexible in terms of what can it store. We don't have to tie to specific schema like it is with relational DBs. As our model changes in time, it is not a big deal for Mongo to adjust to it. It makes it easier to design application from the domain perspective, rather than from data perspective. Having this in mind, we would like to store different structures within our table through the code. Official Micronaut Guide is a good place to start, but it took me a while to learn how to store in Mongo objects containing other objects. Here is the solution.

## Foundation
In this case I will extend a bit application I've presented in my previous post on [integration testing](https://dev.to/michaltalaga/integration-tests-with-micronaut-and-kotlin-5cal). I would like to create dictionary structure, which will hold word in one language together with its translations. To achieve that I've prepared following structure:

```
data class Word(val word: String, val translations: List<Translation>)  
data class Translation(val language: String, val translation: String)
```

If we would like to store it within relational database, by default it would require two tables - one for `Word` rows, and one for `Translation` rows with reference to particular `Word`. Mongo by default allow us to both of these objects within one table. It will contain a `Word` together with list of `Translation` in JSON format as a separate field.

In terms of Mongo, above setup is easily achievable. We can create table with two fields `word` and `translations` where first one will be string value, and the latter one will be JSON containing list of objects having `language` and `translation` fields.

## Serde
Micronaut comes with dedicated plugin called `micronaut-serde-processor` enabling us to **ser**ialize and **de**serialize. We can annotate class with `@Serdeable` annotation to mark it as the one which will be exchanged in the future. As we are not using `micronaut-data` which can make things easier (but I was not able to achieve such nested serialization) we will need to rely on manual poinitng how to serialize to and from `BSON` fields used by Mongo. To enable classes being manipulated such way, we also need to add `@Introspected` annotation.

As mentioned previously, we will have to point out how to convert our entities. The easiest way would be to do that through the constructor. For it to work, we need to mark our constructor with `@Creator` and `@BsonCreator` annotations. Our entity will be converted through the constructor, containing all required fields. For proper conversion, we need as well to show which fields will be taken into consideration. Each one of them needs to be annotated by `@field:BsonProperty("name")` and `@param:BsonProperty("name")` annotations. This is to mark the property as both class (`field`) and constructor(`param`) properties. Having such prepared class, we do not have to worry about declaration of setters and getters being by default key for serialization process. Our classes will look like that:

* MongoWord
```
@Introspected  
@Serdeable  
data class MongoWord @Creator @BsonCreator constructor(  
    @field:BsonProperty("word") @param:BsonProperty("word") val word: String,  
    @field:BsonProperty("translations") @param:BsonProperty("translations") val translations: List<MongoTranslation>  
)
```
* MongoTranslation
```
@Introspected  
@Serdeable  
data class MongoTranslation @Creator @BsonCreator constructor(  
    @field:BsonProperty("language") @param:BsonProperty("language") val language: String,  
    @field:BsonProperty("translation") @param:BsonProperty("translation") val translation: String  
)
```

## Separation of Domain and Entity
It is good practice to separate classes used within our domain logic from the ones being used to communicate with outer world. I like the ability to quickly convert each way eg. through *static factory method*. In Kotlin we can achieve that using *companion object*. Such object will look like following for our *Word* class:

```
companion object {  
    fun fromWord(word: Word): MongoWord {  
        return MongoWord(word.word, word.translations.map { MongoTranslation.fromTranslation(it) })  
    }  
}
```
When we want to create domain object straight out of our entity, we can use method being executed on the instance
```
fun toWord(): Word {  
    return Word(word, translations.map { it.toTranslation() })  
}
```

Having this methods within transport classes will allow us to hide implementation details from domain *Word* and *Translation* object. Thanks to this we can focus on actual business logic, without thinking how our objects should be serialized and deserialized.

## Repository

Having everything prepared there is nothing else than building an repository. This will be a Singleton, which will accept and return *Word* object. As fields used to build it we need `MongoClient` together with names of database and collection which we will operate on. Then all we have to do, is to implement methods responsible for storing and getting *Words* from repository. Below is code showing how we can achieve that.

```
@Singleton  
class WordRepositoryMongo(  
    mongoClient: MongoClient,  
    @Property(name = "word.database") databaseName: String,  
    @Property(name = "word.collection") collectionName: String  
) : WordRepository {  
  
    private val collection: MongoCollection<MongoWord>  
  
    init {  
        val db = mongoClient.getDatabase(databaseName)  
        collection = db.getCollection(collectionName, MongoWord::class.java)  
    }  
  
    override fun findWord(word: String): Word? {  
        return collection.find(eq("word", word)).firstOrNull()?.toWord()  
    }  
  
    override fun putWord(word: Word) {  
        collection.insertOne(MongoWord.fromWord(word))  
    }  
}
```

## Tests
`Testcontainers` are really powerful tool which allows us to test all the code we have just written against actual MongoDB instance. Thanks to micronaut `io.micronaut.test-resources` plugin, the only thing we need to do is to provide dependency to `Testcontainers` and everything would plug in out-of-the-box. No configuration needed. Before writing test, we need to make sure that with each execution the DB state will be cleared. To do this, we can do following:

```
@BeforeEach  
fun beforeEach() {  
    mongoClient.getDatabase(databaseName)  
        .getCollection(collectionName)  
        .deleteMany(Document())  
}
```
It uses injected `MongoClient` like we use it in `WordRepositoryMongo` class. From collection declared as class field, we will delete all existing documents. When we have it prepared, then we can execute the sample test.
```
@Test  
fun shouldStoreWordInRepository() {  
    //Given  
    val word = Word(  
        "hello", listOf(  
            Translation("polish", "czesc"),  
            Translation("deutsch", "hallo")  
        )  
    )  
  
    //When  
    repository.putWord(word)  
  
    //Then  
    val wordFromRepository = repository.findWord("hello")  
    Assertions.assertTrue(wordFromRepository != null)  
    Assertions.assertTrue(wordFromRepository!!.translations.size == 2)  
    Assertions.assertTrue(wordFromRepository!!.translations  
        .filter { it.language == "polish" && it.translation == "czesc" }  
        .size == 1)  
    Assertions.assertTrue(wordFromRepository!!.translations  
        .filter { it.language == "deutsch" && it.translation == "hallo" }  
        .size == 1)  
}
```

It tests if word put could be reached out later.

## Conclusion

It was not an easy job for me to find how to store object structure as parameter for Mongo table. Micronaut is still not so popular as Spring, so the community support is not yet so active. I hope that this article could help you design tables which will realise full potential of domain, without need to think about configuration nitpicks.

All the code used in this article you can find [here](https://github.com/mtalaga/micronaut-kotlin-integration-test) within `hello` package.