Storing case classes in a MongoDB database is incredibly easy, once you know how. The same goes for java.time
classes such as ZonedDateTime
, LocalDate
or Duration
.
This example uses the official Mongo Scala Driver in version 2.x and the bsoncodec project by Ralph Schaer.
The solution
First: A complete working example:
import java.math.BigDecimal
import java.time.{LocalDate, LocalDateTime}
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.language.postfixOps
object MongoDbExample {
def main(args: Array[String]): Unit = {
val myParcel = Parcel(
sender = Address("Online Shop 3000", "E-Commerce-Street", "Berlin"),
receiver = Address("Jannik", "Fun-Street", "Hamburg"),
sent = LocalDate.of(2017, 8, 12),
received = Some(LocalDateTime.of(2017, 8, 19, 10, 24)),
contents = Seq(Content("Cool Robot", new BigDecimal("39.99")),
Content("Drone", new BigDecimal("199.99")))
)
Await.result(DB.parcels.insertOne(myParcel).toFuture, 10 seconds)
val allParcels = Await.result(DB.parcels.find().toFuture(), 10 seconds)
allParcels.foreach(println)
}
}
case class Parcel(sender: Address,
receiver: Address,
sent: LocalDate,
received: Option[LocalDateTime],
contents: Seq[Content])
case class Address(name: String, street: String, city: String)
case class Content(name: String, price: BigDecimal)
object DB {
import ch.rasc.bsoncodec.math._
import ch.rasc.bsoncodec.time._
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.configuration.CodecRegistries._
import org.mongodb.scala.bson.codecs.DEFAULT_CODEC_REGISTRY
import org.mongodb.scala.bson.codecs.Macros._
import org.mongodb.scala.{MongoClient, MongoCollection, MongoDatabase}
private val customCodecs = fromProviders(classOf[Parcel],
classOf[Address],
classOf[Content])
private val javaCodecs = CodecRegistries.fromCodecs(
new LocalDateTimeDateCodec(),
new LocalDateDateCodec(),
new BigDecimalStringCodec())
private val codecRegistry = fromRegistries(customCodecs,
javaCodecs,
DEFAULT_CODEC_REGISTRY)
private val database: MongoDatabase = MongoClient().getDatabase("TrackingData")
.withCodecRegistry(codecRegistry)
val parcels: MongoCollection[Parcel] = database.getCollection("Parcels")
}
This results in the following entry:
{
"_id" : ObjectId("5997f9ff96ddbad1d424981d"),
"sender" : {
"name" : "Online Shop 3000",
"street" : "E-Commerce-Street",
"city" : "Berlin"
},
"receiver" : {
"name" : "Jannik",
"street" : "Fun-Street",
"city" : "Hamburg"
},
"sent" : ISODate("2017-08-12T00:00:00.000Z"),
"received" : ISODate("2017-08-19T10:24:00.000Z"),
"contents" : [
{
"name" : "Cool Robot",
"price" : "39.99"
},
{
"name" : "Drone",
"price" : "199.99"
}
]
}
How to get there
Adding codecs for custom classes
The magic is obviously hidden in the DB
object. Following just the driver documentation, one might start with this simpler version:
object DB {
import org.mongodb.scala.{MongoClient, MongoCollection, MongoDatabase}
private val database: MongoDatabase = MongoClient().getDatabase("TrackingData")
val parcels: MongoCollection[Parcel] = database.getCollection("Parcels")
}
This results in the exception
Exception in thread "main" org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class de.jannikarndt.MongoDbExample.Parcel.
Googleing for the problem, you find quite many outdated solutions, such as salat, which is still maintained but uses the outdated casbah driver (aka “version 1”).
Alex Landa explains, that version 2 of the driver fixes this problem with macros:
The Codec class is generated during the compile time and then added to the default codec registry (using the fromRegistries method).
So the next step is to create codecs for all custom classes and tell the MongoClient to use these codecs:
object DB {
import org.bson.codecs.configuration.CodecRegistries._
import org.mongodb.scala.bson.codecs.DEFAULT_CODEC_REGISTRY
import org.mongodb.scala.bson.codecs.Macros._
import org.mongodb.scala.{MongoClient, MongoCollection, MongoDatabase}
private val customCodecs = fromProviders(classOf[Parcel],
classOf[Address],
classOf[Content])
private val codecRegistry = fromRegistries(customCodecs, DEFAULT_CODEC_REGISTRY)
private val database: MongoDatabase = MongoClient().getDatabase("TrackingData")
.withCodecRegistry(codecRegistry)
val parcels: MongoCollection[Parcel] = database.getCollection("Parcels")
}
Adding codecs for system classes
This brings us one step further, to the next exception:
Exception in thread "main" org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class java.time.LocalDate.
If you try to add classOf[LocalDate]
to the list, you get a compile error, stating that
Error:(41, 106) java.time.LocalDate does not have a constructor
private val customCodecs = fromProviders(classOf[Parcel], classOf[Address], classOf[Content], classOf[LocalDate])
You (or a macro you invoke) cannot add code to the standard java classes. At this point, you would have to write a codec
yourself, extending / implementing org.bson.codecs.Codec
and overriding encode
and decode
.
Luckily, Ralph Schaer has already done this — not just for LocalDate but for most of java.time
, java.sql
, URLs, BigDecimal, javax.money
, and more, and for many types you can even decide how to encode them, i.e. to date
, document
or string
.
That’s what the lines
private val javaCodecs = CodecRegistries.fromCodecs(
new LocalDateTimeDateCodec(),
new LocalDateDateCodec(),
new BigDecimalStringCodec())
private val codecRegistry = fromRegistries(customCodecs,
javaCodecs,
DEFAULT_CODEC_REGISTRY)
in the final solution (see top of the page) are about.