ReactiveMongo is an extremely convenient toolkit for working with MongoDB in Scala applications. But, at the same time, its documentation does not cover some of the typical scenarios, so sometimes it takes time to find the right solution. One of such tasks – storing polymorphic objects in a database – is the focus of this blog post.
Let’s assume that we have a case class Delivery
which contains a list of delivery reports from various providers. And the DeliveryReport
is a trait which must be mixed in by specific provider implementations. The Delivery
class can look like the following:
Delivery.scala
package models
import play.api.libs.json.Json
import reactivemongo.bson.{BSONDocument, BSONHandler}
case class Delivery(reports: List[DeliveryReport])
object Delivery {
implicit val deliveryFormat = Json.format[Delivery]
implicit object OwnerBSONHandler extends BSONHandler[BSONDocument, Delivery] {
override def read(doc: BSONDocument): Delivery = Delivery(
doc.getAs[List[DeliveryReport]]("reports").get
)
override def write(delivery: Delivery): BSONDocument = BSONDocument(
"reports" -> delivery.reports
)
}
}
Every delivery report must contain an essential set of mandatory fields as well as some additional fields which are defined by providers based on the specification of their products. This way, we need the ability to do the appropriate serialization at execution time. Let’s get back to this question a little bit later and have a look at the model classes now:
DeliveryReport.scala
package models
import java.util.Date
trait DeliveryReport {
val baseInfo: BaseInfo
def printReport(): Unit = { /* Report printing implementation... */ }
}
case class BaseInfo(
order: String,
orderDate: Date,
customerId: Long,
deliveryMethod: String,
shippingAddress: String)
/* Specific implementations of provider reports */
case class ClothesProviderReport(baseInfo: BaseInfo) extends DeliveryReport
case class FurnitureProviderReport(
baseInfo: BaseInfo,
needInstall: Boolean,
manufacturer: String,
parts: List[String]) extends DeliveryReport
case class FoodProviderReport(baseInfo: BaseInfo, frozen: Boolean) extends DeliveryReport
As you can see, we have three implementations of the report model for three different providers:
ClothesProviderReport
FurnitureProviderReport
FoodProviderReport
All of them extend DeliveryReport
trait as well as contain the mandatory baseInfo
field and the printReport
method. Some of the reports also have additional fields that describe properties, which are specific to a certain type of product.
In order to store reports in a database, implicit Json formatters should be created for every DeliveryReport
implementation as well as BSONReaders and BSONWriters. In this example, I have created the ReportUtils.scala object that contains all the Json formatters and BSON readers & writers:
ReportUtils.scala
package com.sg.examples
import java.util.Date
import models._
import play.api.libs.json.Json
import reactivemongo.bson.BSONDocument
object ReportUtils {
object Implicits {
implicit val baseInfoFormat = Json.format[BaseInfo]
implicit val clothesFormat = Json.format[ClothesProviderReport]
implicit val furnitureFormat = Json.format[FurnitureProviderReport]
implicit val foodFormat = Json.format[FoodProviderReport]
}
/* BEGIN: BSON readers */
def baseInfoRead(doc: BSONDocument) = BaseInfo(
doc.getAs[String]("order").get,
doc.getAs[Date]("orderDate").get,
doc.getAs[Long]("customerId").get,
doc.getAs[String]("deliveryMethod").get,
doc.getAs[String]("shippingAddress").get
)
def clothesRead(doc: BSONDocument) = ClothesProviderReport(
doc.getAs[BaseInfo]("baseInfo").get
)
def furnitureRead(doc: BSONDocument) = FurnitureProviderReport(
doc.getAs[BaseInfo]("baseInfo").get,
doc.getAs[Boolean]("needInstall").get,
doc.getAs[String]("manufacturer").get,
doc.getAs[List[String]]("parts").get
)
def foodRead(doc: BSONDocument) = FoodProviderReport(
doc.getAs[BaseInfo]("baseInfo").get,
doc.getAs[Boolean]("frozen").get
)
/* END: BSON readers */
/* BEGIN: BSON writers */
def baseInfoWrite(info: BaseInfo) = BSONDocument(
"order" -> info.order,
"orderDate" -> info.orderDate,
"customerId" -> info.customerId,
"deliveryMethod" -> info.deliveryMethod,
"shippingAddress" -> info.shippingAddress
)
def clothesWrite(report: ClothesProviderReport) = BSONDocument(
"baseInfo" -> report.baseInfo
)
def furnitureWrite(report: FurnitureProviderReport) = BSONDocument(
"baseInfo" -> report.baseInfo,
"needInstall" -> report.needInstall,
"manufacturer" -> report.manufacturer,
"parts" -> report.parts
)
def foodWrite(report: FoodProviderReport) = BSONDocument(
"baseInfo" -> report.baseInfo,
"frozen" -> report.frozen
)
/* END: BSON writers */
}
Code language: JavaScript (javascript)
The ReportUtils.scala
object is nothing but a holder for the DeliveryReport
implicits and BSONReaders and BSONWriters for database models.
At this point we have all our objects ready to be stored in a database. Since we do not store reports as instances of specific implementation types, but as instances of DeliveryReport
trait, there is still a question of how the program is going to determine which one should be serialized at runtime. To resolve this, I have created the DeliveryReport
companion object which contains two essential things: custom apply/unapply methods and implicit BSONHandler. Please, take a look at the code snippet to see an example implementation of two companion objects, for the DeliveryReport
trait and for the BaseInfo
case class, which contains only BSONHandler.
DeliveryReport.scala
package models
import play.api.libs.json.{JsValue, Json}
import com.sg.examples.ReportUtils.Implicits._
import com.sg.examples.ReportUtils._
import reactivemongo.bson.{BSONDocument, BSONHandler}
object BaseInfo {
implicit object BaseInfoBSONHandler extends BSONHandler[BSONDocument, BaseInfo] {
override def read(bson: BSONDocument): BaseInfo = baseInfoRead(bson)
override def write(info: BaseInfo): BSONDocument = baseInfoWrite(info)
}
}
object DeliveryReport {
implicit val deliveryFormat = Json.format[DeliveryReport]
/**
* Converts DeliveryReport trait into one of its instances as a JSON.
*/
def unapply(report: DeliveryReport): Option[(String, JsValue)] = {
val (prod: Product, sub) = report match {
case c: ClothesProviderReport => (c, Json.toJson(c)(clothesFormat))
case fu: FurnitureProviderReport => (fu, Json.toJson(fu)(furnitureFormat))
case fo: FoodProviderReport => (fo, Json.toJson(fo)(foodFormat))
}
Some(prod.productPrefix -> sub)
}
/**
* Converts a JSON object into the DeliveryReport instance.
*/
def apply(`class`: String, data: JsValue): DeliveryReport = {
(`class` match {
case "ClothesProviderReport" => Json.fromJson[ClothesProviderReport](data)(clothesFormat)
case "FurnitureProviderReport" => Json.fromJson[FurnitureProviderReport](data)(furnitureFormat)
case "FoodProviderReport" => Json.fromJson[FoodProviderReport](data)(foodFormat)
}).get
}
/**
* Implicitly reads/writes the DeliveryReport trait into/from a BSONDocument.
*/
implicit object DeliveryBSONHandler extends BSONHandler[BSONDocument, DeliveryReport] {
override def read(bson: BSONDocument): DeliveryReport = bson.productPrefix match {
case "ClothesProviderReport" => clothesRead(bson)
case "FurnitureProviderReport" => furnitureRead(bson)
case "FoodProviderReport" => foodRead(bson)
}
override def write(report: DeliveryReport): BSONDocument = report match {
case c: ClothesProviderReport => clothesWrite(c)
case fu: FurnitureProviderReport => furnitureWrite(fu)
case fo: FoodProviderReport => foodWrite(fo)
}
}
}
Code language: JavaScript (javascript)
The custom unapply method is responsible for detecting what kind of report we are dealing with. The apply method, in turn, works the same way, but in the opposite direction.
A service that handles the database access can look like this:
DeliveryReportService.scala
import reactivemongo.api.collections.bson._
val reportCollection: BSONCollection = ???
def store(report: DeliveryReport): Future[WriteResult] =
reportCollection.insert(report) /* ... or process the result somehow */
Code language: JavaScript (javascript)
Finally, storing any kind of the DeliveryReport case classes into a database is going to be performed in the following way.
AnyClass.scala
val furnitureReport = FurnitureProviderReport(
baseInfo: BaseInfo(
order = "orderId1",
orderDate = System.currentTimeMillis,
customerId = 0,
deliveryMethod = "auto",
shippingAddress = "Elm St. 10"
),
parts: List("chair legs", "chair seat")
)
val foodReport = FoodProviderReport(
baseInfo = BaseInfo(
order = "orderId2",
orderDate = System.currentTimeMillis,
customerId = 123,
deliveryMethod = "avia",
shippingAddress = "Elm St. 15"
),
frozen = true
)
DeliveryReportService.store(furnitureReport)
DeliveryReportService.store(foodReport)
Code language: PHP (php)
As a result, we get report documents, stored in the mongodb collection:
FurnitureProviderReport_mongodb_document
{
class: "FurnitureProviderReport",
data: {
baseInfo: {
order: "orderId1",
orderDate: "1467101459346",
customerId: "0",
deliveryMethod: "auto",
shippingAddress: "Elm St. 10"
},
parts: [
"chair legs",
"chair seat"
]
}
},
{
class: "FoodProviderReport",
data: {
baseInfo: {
order: "orderId2",
orderDate: "1467101456354",
customerId: "123",
deliveryMethod: "avia",
shippingAddress: "Elm St. 15"
},
frozen: true
}
}
Code language: JavaScript (javascript)
Note! The example above has been created using the following software versions:
– ReactiveMongo: 0.11.13
– MongoDB: 3.2.2
– Play Framework: 2.5.x
– Scala: 2.11.7