Isn't there any other way to convert bson objects to case class and the vice-versa rather than the implicit conversion in scala? - mongodb

I have went through some of the reactive mongo example and find the conversion of the model and the bson object as below.
case class Artist(
name: String,
slug: String,
artistId: Int,
albums: Vector[Album] = Vector(),
id: ObjectId = new ObjectId())
object ArtistMap {
def toBson(artist: Artist): DBObject = {
MongoDBObject(
"name" -> artist.name,
"slug" -> artist.slug,
"artistId" -> artist.artistId,
"albums" -> artist.albums.map(AlbumMap.toBson),
"_id" -> artist.id
)
}
def fromBson(o: DBObject): Artist = {
Artist(
name = o.as[String]("name"),
slug = o.as[String]("slug"),
artistId = o.as[Int]("artistId"),
albums = o.as[MongoDBList]("albums").toVector
.map(doc => AlbumMap.fromBson(doc.asInstanceOf[DBObject])),
id = o.as[ObjectId]("_id")
)
}
}
Is there any other way to get rid of this overhead of mapping each field of the case classes, maybe some framework over reactivemongo or any utility for this?

I don't understand your comment too, but my assumption you want functionality like that.
(I haven't written on scala for a few month, so sorry for any stupid mistake.)
case class Album(name: String, year: Int)
/*
then you may implement serialization interface(trait) or use macros
*/
implicit val albumHandler = Macros.handler[Album]
/*
I'm not sure is there build in implementation for the Vector[].
*/
object VectorHandler[T](implicit handler: BSONHandler[BSONValue, T]) extends BSONHandler[BSONArray, Vector[T]]{
def read(bson: BSONArray): Vector[T]{
/*
iterate over array and serialize it in a Vector with a help of the handler,
or just persist Vector[] as List[]
*/
}
def write(t: Vector[T]): BSONArray {
}
}
implicit val albumVectorHandler = VectorHandler[Album]()
implicit val artistHandler = Macros.handler[Atrist]

Related

DSL in scala using case classes

My use case has case classes something like
case class Address(name:String,pincode:String){
override def toString =name +"=" +pincode
}
case class Department(name:String){
override def toString =name
}
case class emp(address:Address,department:Department)
I want to create a DSL like below.Can anyone share the links about how to create a DSL and any suggestions to achieve the below.
emp.withAddress("abc","12222").withDepartment("HR")
Update:
Actual use case class may have more fields close to 20. I want to avoid redudancy of code
I created a DSL using reflection so that we don't need to add every field to it.
Disclamer: This DSL is extremely weakly typed and I did it just for fun. I don't really think this is a good approach in Scala.
scala> create an Employee where "homeAddress" is Address("a", "b") and "department" is Department("c") and that_s it
res0: Employee = Employee(a=b,null,c)
scala> create an Employee where "workAddress" is Address("w", "x") and "homeAddress" is Address("y", "z") and that_s it
res1: Employee = Employee(y=z,w=x,null)
scala> create a Customer where "address" is Address("a", "b") and "age" is 900 and that_s it
res0: Customer = Customer(a=b,900)
The last example is the equivalent of writing:
create.a(Customer).where("address").is(Address("a", "b")).and("age").is(900).and(that_s).it
A way of writing DSLs in Scala and avoid parentheses and the dot is by following this pattern:
object.method(parameter).method(parameter)...
Here is the source:
// DSL
object create {
def an(t: Employee.type) = new ModelDSL(Employee(null, null, null))
def a(t: Customer.type) = new ModelDSL(Customer(null, 0))
}
object that_s
class ModelDSL[T](model: T) {
def where(field: String): ValueDSL[ModelDSL2[T], Any] = new ValueDSL(value => {
val f = model.getClass.getDeclaredField(field)
f.setAccessible(true)
f.set(model, value)
new ModelDSL2[T](model)
})
def and(t: that_s.type) = new { def it = model }
}
class ModelDSL2[T](model: T) {
def and(field: String) = new ModelDSL(model).where(field)
def and(t: that_s.type) = new { def it = model }
}
class ValueDSL[T, V](callback: V => T) {
def is(value: V): T = callback(value)
}
// Models
case class Employee(homeAddress: Address, workAddress: Address, department: Department)
case class Customer(address: Address, age: Int)
case class Address(name: String, pincode: String) {
override def toString = name + "=" + pincode
}
case class Department(name: String) {
override def toString = name
}
I really don't think you need the builder pattern in Scala. Just give your case class reasonable defaults and use the copy method.
i.e.:
employee.copy(address = Address("abc","12222"),
department = Department("HR"))
You could also use an immutable builder:
case class EmployeeBuilder(address:Address = Address("", ""),department:Department = Department("")) {
def build = emp(address, department)
def withAddress(address: Address) = copy(address = address)
def withDepartment(department: Department) = copy(department = department)
}
object EmployeeBuilder {
def withAddress(address: Address) = EmployeeBuilder().copy(address = address)
def withDepartment(department: Department) = EmployeeBuilder().copy(department = department)
}
You could do
object emp {
def builder = new Builder(None, None)
case class Builder(address: Option[Address], department: Option[Department]) {
def withDepartment(name:String) = {
val dept = Department(name)
this.copy(department = Some(dept))
}
def withAddress(name:String, pincode:String) = {
val addr = Address(name, pincode)
this.copy(address = Some(addr))
}
def build = (address, department) match {
case (Some(a), Some(d)) => new emp(a, d)
case (None, _) => throw new IllegalStateException("Address not provided")
case _ => throw new IllegalStateException("Department not provided")
}
}
}
and use it as emp.builder.withAddress("abc","12222").withDepartment("HR").build().
You don't need optional fields, copy, or the builder pattern (exactly), if you are willing to have the build always take the arguments in a particular order:
case class emp(address:Address,department:Department, id: Long)
object emp {
def withAddress(name: String, pincode: String): WithDepartment =
new WithDepartment(Address(name, pincode))
final class WithDepartment(private val address: Address)
extends AnyVal {
def withDepartment(name: String): WithId =
new WithId(address, Department(name))
}
final class WithId(address: Address, department: Department) {
def withId(id: Long): emp = emp(address, department, id)
}
}
emp.withAddress("abc","12222").withDepartment("HR").withId(1)
The idea here is that each emp parameter gets its own class which provides a method to get you to the next class, until the final one gives you an emp object. It's like currying but at the type level. As you can see I've added an extra parameter just as an example of how to extend the pattern past the first two parameters.
The nice thing about this approach is that, even if you're part-way through the build, the type you have so far will guide you to the next step. So if you have a WithDepartment so far, you know that the next argument you need to supply is a department name.
If you want to avoid modifying the origin classes you can use implicit class, e.g.
implicit class EmpExtensions(emp: emp) {
def withAddress(name: String, pincode: String) {
//code omitted
}
// code omitted
}
then import EmpExtensions wherever you need these methods

Insert document with ReactiveMongo does not use BSONWriter or Reader

I have the following class:
case class DeviceRegistration(deviceData: DeviceData,
pin: String,
created: DateTime) {}
Type DeviceData is defined simply as 4 string fields.
I've been trying unsuccessfully to insert a type of DeviceRegistration to a mongo collection. I want to make sure the date is stored as a ISODate and not a NumberLong, so I implemented custom readers and writers.
implicit object DeviceDataWriter extends BSONDocumentWriter[DeviceData] {
def write(data: DeviceData): BSONDocument = BSONDocument(
DEVICE_ID_KEY -> data.deviceId,
MODEL_KEY -> data.model,
BRAND_KEY -> data.brand,
MANUFACTURER_KEY -> data.manufacturer)
}
implicit object DeviceRegistrationWriter extends BSONDocumentWriter[DeviceRegistration] {
def write(registration: DeviceRegistration): BSONDocument = BSONDocument(
DEVICE_DATA_KEY -> registration.deviceData,
PIN_KEY -> registration.pin,
CREATED_KEY -> BSONDateTime(registration.created.getMillis))
}
implicit object DeviceDataReader extends BSONDocumentReader[DeviceData] {
def read(doc: BSONDocument): DeviceData = {
val deviceId = doc.getAs[String](DEVICE_ID_KEY).get
val model = doc.getAs[String](MODEL_KEY)
val brand = doc.getAs[String](BRAND_KEY)
val manufacturer = doc.getAs[String](MANUFACTURER_KEY)
DeviceData(deviceId, model, brand, manufacturer)
}
}
implicit object DeviceRegistrationReader extends BSONDocumentReader[DeviceRegistration] {
def read(doc: BSONDocument): DeviceRegistration = {
val deviceData = doc.getAs[DeviceData](DEVICE_DATA_KEY).get
val pin = doc.getAs[String](PIN_KEY).get
val created = doc.getAs[BSONDateTime](CREATED_KEY).map(dt => new DateTime(dt.value))
DeviceRegistration(deviceData, pin, created.get)
}
}
I'm trying to insert the document with the following code:
def save(deviceRegistration: DeviceRegistration): Future[DeviceRegistration] = {
deviceRegistrations.insert(deviceRegistration).map(result => deviceRegistration)
}
To retrieve, I'm doing this:
def findDeviceRegistrationRequest(deviceConfirmationData: DeviceConfirmationData) = {
deviceRegistrations.find(BSONDocument("pin" -> deviceConfirmationData.pin))
.one[DeviceRegistration](ReadPreference.Primary)
}
The record is stored as this:
{ "_id" : ObjectId("56dea8d0d8cadd6ff70690d8"), "deviceData" : { "deviceId" : "kdsajkldjsalkda" }, "pin" : "9914", "created" : NumberLong("1457432783921") }
The created date is clearly not being serialized by my writer. It seems reactivemongo is using some default writer.
Likewise, when I read, I get an exception:
Caused by: java.lang.RuntimeException: (/created,List(ValidationError(List(error.expected.date),WrappedArray())))
So it's clearly also not using my reader.
I didn't have any luck googling around. What am I missing?
I also tried to find a way to specifically set the writer and reader I want to use (instead of relying on the implicit mechanism), but I was not able to figure it out.
Any pointers in the right direction would be most appreciated.

ReactiveMongo: How to convert a BSONArray to List[String]

Given the following BSONDocument...
val values = BSONDocument("values" -> BSONArray("one", "two", "three"))
How do I convert it to a List? I've tried this...
values.getAs[List[String]]("values").getOrElse(List.empty))
... but it doesn't work - I always get List.empty.
Am I missing something?
EDIT
OK... I think it is worth describing the real case. I ran the distinct command and this was the result:
values: {
values: [
0: BSONObjectID("55d0f641a100000401b7e454")
],
stats: {
n: BSONInteger(1),
nscanned: BSONInteger(1),
nscannedObjects: BSONInteger(1),
timems: BSONInteger(0),
cursor: BSONString(BtreeCursor projectId_1)
},
ok: BSONDouble(1.0)
}
I need to transform values to a Scala List[String] like this:
List("55d0f641a100000401b7e454")
Here is my solution. First, I've defined a BSONReader[BSONValue, String] like this...
package object bsonFormatters {
implicit object BSONValueStringReader extends BSONReader[BSONValue, String] {
def read(bson: BSONValue) = bson match {
case oid: BSONObjectID => oid.stringify
}
}
... and then just imported it in my companion object like this:
import reactivemongo.bson.{BSONString, BSONDocument}
import reactivemongo.core.commands.{CommandError, BSONCommandResultMaker, Command}
case class Distinct(
collectionName: String,
field: String,
query: Option[BSONDocument] = None
) extends Command[Seq[String]] {
override def makeDocuments = BSONDocument(
"distinct" -> BSONString(collectionName),
"key" -> field,
"query" -> query
)
val ResultMaker = Distinct
}
object Distinct extends BSONCommandResultMaker[Seq[String]] {
import bsonFormatters._ // this makes the trick
def apply(document: BSONDocument) = CommandError.checkOk(
document,
Some("distinct")
).toLeft(
document.getAs[List[String]]("values").getOrElse(List.empty)
)
}
I hope it helps.

Scala serialization exception with Enumeration Value

I'm using the play 2.1 framework for scala and the MongoDB Salat plugin.
When I update an Enumeration.Value I got an exception:
java.lang.IllegalArgumentException: can't serialize class scala.Enumeration$Val
at org.bson.BasicBSONEncoder._putObjectField(BasicBSONEncoder.java:270) ~[mongo-java-driver-2.11.1.jar:na]
at org.bson.BasicBSONEncoder.putIterable(BasicBSONEncoder.java:295) ~[mongo-java-driver-2.11.1.jar:na]
at org.bson.BasicBSONEncoder._putObjectField(BasicBSONEncoder.java:234) ~[mongo-java-driver-2.11.1.jar:na]
at org.bson.BasicBSONEncoder.putObject(BasicBSONEncoder.java:174) ~[mongo-java-driver-2.11.1.jar:na]
at org.bson.BasicBSONEncoder.putObject(BasicBSONEncoder.java:120) ~[mongo-java-driver-2.11.1.jar:na]
at com.mongodb.DefaultDBEncoder.writeObject(DefaultDBEncoder.java:27) ~[mongo-java-driver-2.11.1.jar:na]
Inserting the Enumeration.Value works fine. My case class looks like:
case class User(
#Key("_id") id: ObjectId = new ObjectId,
username: String,
email: String,
#EnumAs language: Language.Value = Language.DE,
balance: Double,
added: Date = new Date)
and my update code:
object UserDAO extends ModelCompanion[User, ObjectId] {
val dao = new SalatDAO[User, ObjectId](collection = mongoCollection("users")) {}
def update(): WriteResult = {
UserDAO.dao.update(q = MongoDBObject("_id" -> new ObjectId(id)), o = MongoDBObject("$set" -> MongoDBObject("language" -> Language.EN))))
}
}
Any ideas how to get that working?
EDIT:
workaround: it works if I cast the Enumeration.Value toString, but that's not how it should be...
UserDAO.dao.update(q = MongoDBObject("_id" -> new ObjectId(id)), o = MongoDBObject("$set" -> MongoDBObject("language" -> Language.EN.toString))))
It is possible to add a BSON encoding for Enumeration. So, the conversion is done in a transparent manner.
Here is the code
RegisterConversionHelpers()
custom()
def custom() {
val transformer = new Transformer {
def transform(o: AnyRef): AnyRef = o match {
case e: Enumeration$Val => e.toString
case _ => o
}
}
BSON.addEncodingHook(classOf[Enumeration$Val], transformer)
}
}
At the time of writing mongoDB doesn't place nice with scala enums, I use a decorator method as a work around.
Say you have this enum:
object EmployeeType extends Enumeration {
type EmployeeType = Value
val Manager, Worker = Value
}
and this mongodb record:
import EmployeeType._
case class Employee(
id: ObjectId = new ObjectId
)
In your mongoDB, store the integer index of the enum instead of the enum itself:
case class Employee(
id: ObjectId = new ObjectId,
employeeTypeIndex: Integer = 0
){
def employeeType = EmployeeType(employeeTypeIndex); /* getter */
def employeeType_=(v : EmployeeType ) = { employeeTypeIndex= v.id} /* setter */
}
The extra methods implement getters and setters for the employee type enum.
Salat only does its work when you serialize to and from your model object with the grater, not when you do queries with MongoDB-objects yourself. The mongo driver api knows nothing about the annotation #EnumAs. (In addition to that even if you could use salat for that, how would it be able to know that you are referring to User.language in a generic key->value MongoDBObject?)
So you have to do like you describe in your workaround. Provide the "value" of the enum yourself when you want to do queries.

How do I map my objects correctly to make a list of objects work in mongo + play2

I'm trying to write readers/writers for my case classes after reading:
https://github.com/sgodbillon/reactivemongo-demo-app/blob/master/app/models/articles.scala
https://github.com/zenexity/ReactiveMongo/blob/master/driver/samples/BSON.scala
but I'm having trouble in making it work.
I have a leadCategory that can consist of several word objects.
package models
import org.joda.time.DateTime
import reactivemongo.bson._
import reactivemongo.bson.handlers.{BSONWriter, BSONReader}
import reactivemongo.bson.BSONDateTime
import reactivemongo.bson.BSONString
LeadCategory:
case class LeadCategory(
id: Option[BSONObjectID],
categoryId: Long,
categoryName: String,
creationDate: Option[DateTime],
updateDate: Option[DateTime],
words: List[Word]
)
object LeadCategory {
implicit object LeadCategoryBSONReader extends BSONReader[LeadCategory] {
def fromBSON(document: BSONDocument): LeadCategory = {
val doc = document.toTraversable
LeadCategory(
doc.getAs[BSONObjectID]("_id"),
doc.getAs[BSONLong]("caregoryId").get.value,
doc.getAs[BSONString]("categoryName").get.value,
doc.getAs[BSONDateTime]("creationDate").map(dt => new DateTime(dt.value)),
doc.getAs[BSONDateTime]("updateDate").map(dt => new DateTime(dt.value)),
doc.getAs[List[Word]]("words").toList.flatten)
}
}
implicit object LeadCategoryBSONWriter extends BSONWriter[LeadCategory] {
def toBSON(leadCategory: LeadCategory) = {
BSONDocument(
"_id" -> leadCategory.id.getOrElse(BSONObjectID.generate),
"caregoryId" -> BSONLong(leadCategory.categoryId),
"categoryName" -> BSONString(leadCategory.categoryName),
"creationDate" -> leadCategory.creationDate.map(date => BSONDateTime(date.getMillis)),
"updateDate" -> leadCategory.updateDate.map(date => BSONDateTime(date.getMillis)),
"words" -> leadCategory.words)
}
}
Word:
case class Word(id: Option[BSONObjectID], word: String)
object Word {
implicit object WordBSONReader extends BSONReader[Word] {
def fromBSON(document: BSONDocument): Word = {
val doc = document.toTraversable
Word(
doc.getAs[BSONObjectID]("_id"),
doc.getAs[BSONString]("word").get.value
)
}
}
implicit object WordBSONWriter extends BSONWriter[Word] {
def toBSON(word: Word) = {
BSONDocument(
"_id" -> word.id.getOrElse(BSONObjectID.generate),
"word" -> BSONString(word.word)
)
}
}
}
I'm getting a compile error at:
"words" -> leadCategory.words)
stating:
Too many arguments for method apply(ChannelBuffer)
Type mismatch found: (String, List[Words]) required required implicits.Producer[(String, BsonValue)]
What have I missed? Maybe I have misunderstood the docs...
Try to declare the "Word" implicit objects above the "LeadCategory", if you are putting all of it in the same file.
Scala cannot find the implicit objects for the List[Word] - assuming you are using ReactiveMongo 0.9.
All of your types should a derivative of a BSON type. I think you want to use BSONArray instead of a List.