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:
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
123456789101112131415161718192021222324252627
packagemodelsimportjava.util.DatetraitDeliveryReport{valbaseInfo:BaseInfodefprintReport():Unit={/* Report printing implementation... */}}caseclassBaseInfo(order:String,orderDate:Date,customerId:Long,deliveryMethod:String,shippingAddress:String)/* Specific implementations of provider reports */caseclassClothesProviderReport(baseInfo:BaseInfo)extendsDeliveryReportcaseclassFurnitureProviderReport(baseInfo:BaseInfo,needInstall:Boolean,manufacturer:String,parts:List[String])extendsDeliveryReportcaseclassFoodProviderReport(baseInfo:BaseInfo,frozen:Boolean)extendsDeliveryReport
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:
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.
packagemodelsimportplay.api.libs.json.{JsValue,Json}importcom.sg.examples.ReportUtils.Implicits._importcom.sg.examples.ReportUtils._importreactivemongo.bson.{BSONDocument,BSONHandler}objectBaseInfo{implicitobjectBaseInfoBSONHandlerextendsBSONHandler[BSONDocument, BaseInfo]{overridedefread(bson:BSONDocument):BaseInfo=baseInfoRead(bson)overridedefwrite(info:BaseInfo):BSONDocument=baseInfoWrite(info)}}objectDeliveryReport{implicitvaldeliveryFormat=Json.format[DeliveryReport]/** * Converts DeliveryReport trait into one of its instances as a JSON. */defunapply(report:DeliveryReport):Option[(String, JsValue)]={val(prod:Product,sub)=reportmatch{casec:ClothesProviderReport=>(c,Json.toJson(c)(clothesFormat))casefu:FurnitureProviderReport=>(fu,Json.toJson(fu)(furnitureFormat))casefo:FoodProviderReport=>(fo,Json.toJson(fo)(foodFormat))}Some(prod.productPrefix->sub)}/** * Converts a JSON object into the DeliveryReport instance. */defapply(`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. */implicitobjectDeliveryBSONHandlerextendsBSONHandler[BSONDocument, DeliveryReport]{overridedefread(bson:BSONDocument):DeliveryReport=bson.productPrefixmatch{case"ClothesProviderReport"=>clothesRead(bson)case"FurnitureProviderReport"=>furnitureRead(bson)case"FoodProviderReport"=>foodRead(bson)}overridedefwrite(report:DeliveryReport):BSONDocument=reportmatch{casec:ClothesProviderReport=>clothesWrite(c)casefu:FurnitureProviderReport=>furnitureWrite(fu)casefo:FoodProviderReport=>foodWrite(fo)}}}
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
1234567
importreactivemongo.api.collections.bson._valreportCollection:BSONCollection=???defstore(report:DeliveryReport):Future[WriteResult]=reportCollection.insert(report)/* ... or process the result somehow */
Finally, storing any kind of the DeliveryReport case classes into a database is going to be performed in the
following way.
AnyClass.scala
12345678910111213141516171819202122232425
valfurnitureReport=FurnitureProviderReport(baseInfo:BaseInfo(order="orderId1",orderDate=System.currentTimeMillis,customerId=0,deliveryMethod="auto",shippingAddress="Elm St. 10"),parts:List("chairlegs","chairseat"))valfoodReport=FoodProviderReport(baseInfo=BaseInfo(order="orderId2",orderDate=System.currentTimeMillis,customerId=123,deliveryMethod="avia",shippingAddress="Elm St. 15"),frozen=true)DeliveryReportService.store(furnitureReport)DeliveryReportService.store(foodReport)
As a result, we get report documents, stored in the mongodb collection:
FurnitureProviderReport_mongodb_document
1234567891011121314151617181920212223242526272829
{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}}
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
If you're looking for a developer or considering starting a new project,
we are always ready to help!