Saturday, October 20, 2018

Setting up work with MongoDB in Scala

MongoDB provides a reactive driver what allows to forget about thread pool when working with the database. Another reason to choose MongoDB it's to store or load case classes without writing a mapping between case class and document.
So, at the begin add a driver to the dependencies:
"org.mongodb.scala" %% "mongo-scala-driver"   % "2.2.1",
"io.netty"           % "netty-transport"      % "4.1.29.Final",
"io.netty"           % "netty-handler"        % "4.1.29.Final"
Create mongo client and open database:
private val mongoClient = MongoClient(config.getString("db.url"))
private val omsDatabase: MongoDatabase = mongoClient.getDatabase("mydb")
Let's define a simple case class
case class User(_id: ObjectId, name: String, password: String)
To save it in the database as it is we need to generate mapping to document and register it to the codec registry
import org.mongodb.scala.bson.codecs.DEFAULT_CODEC_REGISTRY
import org.bson.codecs.configuration.CodecRegistries.{ fromProviders, fromRegistries }
import org.mongodb.scala.bson.codecs.Macros._

private val codecRegistry = fromRegistries(fromProviders(classOf[User]), DEFAULT_CODEC_REGISTRY)
Now let's open collection with this codec registry
private val collection: MongoCollection[User] = db.withCodecRegistry(codecRegistry).getCollection("collectionName")
That is all, now we can save and load our case class
collection.insertOne(User(new ObjectId, "username", "password"))
val users: Observable[User] = collection.find()
But to case class User has two issues if you want to send it through http as json. First of all spray does not know how to work with class ObjectId, second - property _id open information what a database is behind. So, to (de)serialize ObjectId we need to define an implicit function
  implicit object ObjectIdFormat extends JsonFormat[ObjectId] {
    def write(x: ObjectId) = JsString(x.toString)
    def read(value: JsValue): ObjectId = value match {
      case JsString(x) => new ObjectId(x)
      case x => deserializationError("Expected hex string, but got " + x)
    }
  }
Next, to serialize _id as id we need to fix a list of names what spray extracs from case classes. Spray does it in the class spray.json.ProductFormats.
trait CustomProductFormats extends ProductFormats {
  this: StandardFormats =>

  override protected def extractFieldNames(classManifest: ClassManifest[_]): Array[String] = {
    val fields = super.extractFieldNames(classManifest)
    fields.map(name => if (name == "_id") "id" else name)
  }
}
And last step is to create a copy of spray.json.DefaultJsonProtocol and to use our CustomProductFormats instead of ProductFormats
object JsonFormats extends BasicFormats
  with StandardFormats
  with CollectionFormats
  with CustomProductFormats
  with AdditionalFormats

No comments:

Post a Comment