In my previous posts, we only did simple CRUD queries and aggregate functions in the Mongo Console. But to really benefit from your MongoDB, you need to be able to use it in your programs. MongoDB provides drivers for multiple programming languages: C, C++, C#, Java, Node.js, Perl, PHP, Python, Ruby, Scala. We’ll have a look at how to use MongoDB to do some queries in Kotlin. We can of course use the Java driver as Kotlin can use any Java library. We’ll read in the CIA factbook from a JSON file, load it into Mongo and perform an aggregate function on it.

You can find the entire source code on Gitlab.

To be able to follow my example, you’ll need a running Mongo DB. In my examples, I don’t specify any ports, so my code will connect to a Mongo on localhost with the default port.

As very first step, you need to add the Mongo Java driver as a dependency. I’m using Gradle, so you have to add the following dependency to your build.gradle file:

dependencies {
  compile "org.jetbrains.kotlin:kotlin-stdlib:1.2.40"
  compile 'org.mongodb:mongodb-driver:3.8.1'
}

I also added the dependency to the Kotlin stdlib as we need it, too. To connect to the Mongo DB using the default ports, you just have to create a MongoClient (com.mongodb.MongoClient). Then you connect to the database you want and access the collection you want to perform operations on (both the database nor the collection don’t need to exist, they will be created as soon as you insert the first document in the collection).

    val mongoClient = MongoClient()
    val database = mongoClient.getDatabase("factbook")
    val collection = database.getCollection("countries", BsonDocument::class.java)

Two quick infos for those not familiar with Kotlin: we don’t need to specify the types for the variables as Kotlin uses type inference. The types for the variables are MongoDatabase and MongoCollection both from the package com.mongodb.client. Second MongoCollection is a generic class which can take either BsonDocument, Document or DBObject as type parameter. DBObject is a legacy class used in old versions of the driver, so I haven’t used it myself. It’s still there for backward compatibility reason.

The classes Document and BsonDocument are both used to represent BSON which is the data format used to represent MongoDB document. It is very similar to JSON and most JSON documents are valid BSON as well. The difference between the two is the following:

BsonDocument is a type-safe container for a BsonDocument, it is an implementation of Map<String, BsonValue>. BsonValue is a wrapper for all types that the value of BSON key-value-pairs can get (e.g. BsonDocument, BsonArray, BsonDouble, BsonBoolean…). A BsonValue object has functions to check its type (e.g. isBsonArray) and functions to get the concrete subclass (e.g. asBsonArray). So you can safely access the members without need for explicit casts.

Document on the other hand, is an implementation of Map<String, Object>, so here you need to cast to get the concrete implementation. It depends on your use case whether BsonDocument or Document is the better choice. When you use Document you can append any Java object using the append(key, value) function of Document. With BsonDocument you will first have to transform it to a BsonValue before you can add it.

So in my opinion, it’s more convenient to use BsonDocument if you want to traverse a document that you have parsed from a JSON string or retrieved from MongoDB and it is more convenient to use Document when you construct a document from Java objects.

As I will parse a JSON document, tranform it slightly and insert it into the “factbook” collection, it is easier to do so using BsonDocument.

Next we will read in the factbook json and parse it as BsonDocument. For this I wrote a small object called BsonImporter which can read in a file from the filesystem or the class path and build a BsonDocument object from it:

import org.bson.BsonDocument
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

object BsonImporter {

    fun resourceToString(resourcePath : String) : String  {
        val jsonPath = BsonImporter::class.java.getResource(resourcePath)
        return fileToString(Paths.get(jsonPath.toURI()));
    }

    fun pathToJson(path : String) : BsonDocument {
        val jsonAsText = fileToString(Paths.get(path))
        return BsonDocument.parse(jsonAsText)
    }

    private fun fileToString(path: Path) : String {
        val lines = Files.readAllLines(path, Charset.forName("UTF-8"));
        val jsonAsText = lines.joinToString(separator = "\n")
        return jsonAsText
    }

    fun resourceToBsonElement(resourcePath: String) : BsonDocument {
        val jsonAsText = resourceToString(resourcePath)
        return BsonDocument.parse(jsonAsText)
    }
}

In short, I read in the file and make a string out of it and then I simply use the BsonDocument.parse method and I get my BsonDocument (there is also a Document.parse method to get a Document).

Next, I’m doing a minor transformation to the document as it is a single document, but I want one document per country. The original structure is something like this:

{"countries":
  {
    "Germany": {"data": {"name": "Germany", "population":...}, "metadata": {...}},
    "USA": {"data": {"name": "USA", "population":...}, "metadata": {...}},
    ...
  }
}

I’m only interested in the “data” part of every element in the “countries” object. So to split the document into a list of documents containing only the “data” attribute, I use following function:

fun splitIntoSingleJsonObjectsPerCountry(): List<BsonDocument> {
    val factbook = BsonImporter.resourceToBsonElement("/factbook.json")
    val countries = factbook.getDocument("countries")
    val list = countries.map { (_, value) ->
        value.asDocument().getDocument("data")
    }
    return list
}

So first I use the function I defined earlier to get the factbook as BsonDocument. In my project I added the json file as resource as the only purpose of this project was to read in the factbook, so I didn’t make it more generic and added the json to the project. Next I access the subdocument using the getDocument method. Exactly this method call is the reason I had to use BsonDocument instead of Document, because Document only has a get(Object) method (and methods like getInteger for primitive types), but no specific method to get a nested document or a nested array.

Next, I use the map function to transform my BsonDocument (which is a Map<String, BsonValue> as you recall) into a List which contains only the "data" attribute.

Finally, now that I have the list of documents, I can insert them all into my collection using the insertMany function.

fun writeCountriesToMongo() {
    val mongoClient = MongoClient()
    val database : MongoDatabase = mongoClient.getDatabase("factbook")
    val collection : MongoCollection<BsonDocument> =
        database.getCollection("countries", BsonDocument::class.java)
    val countriesAsBsonDocuments = splitIntoSingleJsonObjectsPerCountry()
    collection.insertMany(countriesAsBsonDocuments)
}

That’s basically all it needs to write a JsonDocument into a MongoDB collection.

Finally, let’s do a query on the collection we did just create. As you might recall, a query in MongoDB had the form collection.find({, }), so the parameters of the find method are BsonDocuments.

Here is a minimal example:

fun getPopulationOfAndorra() {
    val mongoClient = MongoClient()
    val database = mongoClient.getDatabase("factbook")
    val collection = database.getCollection("countries", BsonDocument::class.java)
    val populationOfAndorra = collection
            .find(BsonDocument("name", BsonString("Andorra")), BsonDocument::class.java)
            .projection(BsonDocument("people.population.total", BsonInt32(1)))
    println("The population of Andorra is: ${populationOfAndorra.first().getDocument("people").getDocument("population").getInt32("total").value}")
}

This is very verbose and not luckily not all that the MongoDB driver has to give. Luckily there are a lot of Filter helpers that make it a bit easier to write filters and projections:

val populationOfAndorra = collection
        .find(eq("name", "Andorra"))
        .projection(fields(include("people.population.total")))

Finally, we will do an aggregation query using the API: we want all countries where French is spoken. For this we need a pipeline that first filters all countries that have French in the language list (people.languages.language), then we need to unwind the documents so that we are able to filter out the othe other languages and finally we will rename the fields and do a projection:

private fun getAllCountriesSpeakingSpecificLanguage(mongoClient: MongoClient, language: String): List<Document> {
  val database = mongoClient.getDatabase("factbook")
  val collection = database.getCollection("countries")
  val aggregate: AggregateIterable<Document> = collection.aggregate(mutableListOf(
    // select all countries with French as language
    match(elemMatch("people.languages.language", eq("name", language))),
    // unwind to get one document per language and country
    unwind("\$people.languages.language"),
    // projection to language and country name
    project(Document("name", 1).append("people.languages", 1).append("_id", 0)),
    // only keep unwinded documents with the searched language
    match(eq("people.languages.language.name", language)),
    // add aliases country and speakersPercentage
    addFields(mutableListOf(
            Field("country", "\$name"),
            Field("speakersPercentage", "\$people.languages.language.percent")
    ) as List<Field<*>>?),
    // project on the aliases
    project(Document("country", 1).append("speakersPercentage", 1))
  ));
  return aggregate.toList()
}

Here an example on how to call the function and print out the results:

val allFrenchSpeakingCountries = getAllCountriesSpeakingSpecificLanguage(mongoClient, language)
allFrenchSpeakingCountries.sortedByDescending { (it.getOrDefault("speakersPercentage", 0) as Number).toDouble() }
    .forEach {
        val country = it.getString("country")
        val percentage = it.getOrDefault("speakersPercentage", "not specified")
        println("In $country the percentage of people speaking $language is $percentage")
    }

Note that in this example we didn’t use Collection but MongoCollection (the default) to retrieve the query result. This has the advantage, that we could read out the number and the country name easier. If our result would have been more nested, BsonDocument might have been the better choice.

Now that you know how to use MongoDB via Kotlin, you can read in my next post, how you can expose your MongoDB layer via REST in Kotlin.

If you are interested in Clojure, you can learn how to use MongoDB in Clojure.