Reputation: 2744
In order to do geospatial queries in MongoDB a document with a location (with a 2d
or 2dsphere
geospatial index) should look something like this:
{
_id: …,
loc: {
type: "Point",
coordinates: [ <longitude>, <latitude> ]
}
}
I'm very new to Scala, ReactiveMongo and Play Framework, but in my opinion an obvious way to use such a location is through a case class like:
case class Point(lon: Double, lat: Double)
And the JSON representation that a website's API deals with should look something like:
{
_id: …
loc: [ <longitude>, <latitude> ]
}
Now, I can't figure out how to tell my ReactiveMongo model to serialize/deserialize between these formats.
My controller looks like this:
package controllers
import play.api._
import play.api.mvc._
import play.api.libs.json._
import scala.concurrent.Future
// Reactive Mongo imports
import reactivemongo.api._
import scala.concurrent.ExecutionContext.Implicits.global
// Reactive Mongo plugin
import play.modules.reactivemongo.MongoController
import play.modules.reactivemongo.json.collection.JSONCollection
object Application extends Controller with MongoController {
def collection: JSONCollection = db.collection[JSONCollection]("test")
import play.api.data.Form
import models._
import models.JsonFormats._
def createCC = Action.async {
val user = User("John", "Smith", Point(-0.0015, 51.0015))
val futureResult = collection.insert(user)
futureResult.map(_ => Ok("Done!"))
}
}
I tried to use a PointWriter and PointReader. This is my models.scala:
package models
import reactivemongo.bson._
import play.modules.reactivemongo.json.BSONFormats._
case class User(
// _id: Option[BSONObjectID],
firstName: String,
lastName: String,
loc: Point)
case class Point(lon: Double, lat: Double)
object Point {
implicit object PointWriter extends BSONDocumentWriter[Point] {
def write(point: Point): BSONDocument = BSONDocument(
"type" -> "Point",
"coordinates" -> Seq(point.lat, point.lon)
)
}
implicit object PointReader extends BSONReader[BSONDocument, Point] {
def read(doc: BSONDocument): Point = Point(88, 88)
}
}
object JsonFormats {
import play.api.libs.json.Json
import play.api.data._
import play.api.data.Forms._
import play.api.libs.json._
import play.api.libs.functional.syntax._
implicit val pointFormat = Json.format[Point]
}
When I call the controller action createCC
I would expect have a properly formatted Point object the newly created document, but what I actually get is something like:
{
"_id": ObjectId("52ac76dd1454bbf6d96ad1f1"),
"loc": {
"lon": -0.0015,
"lat": 51.0015
}
}
So my attempt to use PointWriter
and PointReader
to tell ReactiveMongo how to write such a Point
object to the database has no effect at all.
Can anybody help me understand what I have to do?
(I come from a PHP background and try to get my head round Scala...)
Update: Thanks to tmbo's answer I came up with this writer:
val pointWrites = Writes[Point]( p =>
Json.obj(
"type" -> JsString("Point"),
"coordinates" -> Json.arr(JsNumber(p.lon), JsNumber(p.lat))
)
)
Upvotes: 4
Views: 1435
Reputation: 1317
The problem you are facing is related to a mixup between JSONCollection
and BSONCollection
.
BSONCollection
is the default collection reactivemongo uses. This implementation needs an implementation of a BSONDocumentWriter
and a BSONReader
for a case class to get (de-)serialised.
JSONCollection
on the other hand is the default collection implementation that the play-reactive module uses. Since you defined the collection to be a JSONCollection
in db.collection[JSONCollection]("test")
you need to provide an implicit json format.
The json format you provide is
implicit val pointFormat = Json.format[Point]
This will serialize an object to the following format
{
"lon": -0.0015,
"lat": 51.0015
}
If you want to serialise your Point
to an array you need to replace the above implicit pointFormat
:
import play.api.libs.json._
import play.api.libs.json.Reads._
case class Point(lng: Double, lat: Double)
object Point {
val pointWrites = Writes[Point]( p => Json.toJson(List(p.lng, p.lat)))
val pointReads = minLength[List[Double]](2).map(l => Point(l(0), l(1)))
implicit val pointFormat = Format(pointReads, pointWrites)
}
You actualy don't need the BSONReader
and BSONDocumentWriter
.
Edit:
Here is a reads that also validates the type
attribute of the document:
val pointReads =
(__ \ 'type).read[String](constraints.verifying[String](_ == "Point")) andKeep
(__ \ 'coordinates).read[Point](minLength[List[Double]](2).map(l => Point(l(0), l(1))))
Upvotes: 5