Updating nested array inside array using reactivemongo - mongodb

I'm trying to update an element in nested lists with scala and reactive mongo. I tried this:
def updatethirdList(secondListElementId: UUID, firstObject: FirstObject): Future[Either[String, UUID]] = {
val query = Json.obj("secondList.thirdList._id" -> secondListElementId)
val update = Json.obj("$set" -> Json.obj("secondList.$.thirdList" -> sessionType))
collection.update(query, update).map {
case result if result.ok == true => Right(secondListElementId)
case result => Left(result.message)
}
}
the structure:
{
"firstList":[{
"secondList":[{
"thirdList":[{
"firstObject":""
}]
}]
}]
}
the problem with my code is that it's gave this result:
firstList.0.secondList.0.thirdList = firstObject
it should give for example this :
firstList.0.secondList.0.thirdList.0 = firstObject
I tried this:
val update = Json.obj("$set" -> Json.obj("secondList.$.thirdList.$" -> firstObject))
but I get this error: MongoDB: Too many positional (i.e. '$') elements found in path
Any help please

Related

Scala flatten a Seq[Future[Seq[]]]

I'm stuck trying to flatten a Seq[Seq[]] with returning the outcome. So what I had was this:
def getListsByLC(lcId: Int): Action[AnyContent] = Action.async {
listRepo.getListsByLC(lcId).flatMap { lists =>
val items: Seq[Future[Seq[Item]]] = lists.map { list =>
itemRepo.getItemsByList(list.id)
}
Future.sequence(items).map { result =>
Ok(Json.obj("lists" -> lists, "items" -> result))
}
}
}
The outcome was obviously an array of arrays
Now what I wanted to do is flatten this Future.sequence in order to only have one array containing all items. This, alongside similar versions I found browsing the web, is what I tried:
def getListsByLC(lcId: Int): Action[AnyContent] = Action.async {
listRepo.getListsByLC(lcId).flatMap { lists =>
val items: Seq[Future[Seq[Item]]] = lists.map { list =>
itemRepo.getItemsByList(list.id)
}
Future.sequence(items).map(._flatten) { result =>
Ok(Json.obj("lists" -> lists, "items" -> result))
}
}
}
Here I get this compiling error:
Sorry for the quality of the screenshot. Any ideas? Thanks in advance!
You need to call _.flatten instead of ._flatten and you're missing an additional .map call after flattening the sequences:
Future.sequence(items).map(_.flatten).map { result =>
Ok(Json.obj("lists" -> lists, "items" -> result))
}

Building a document functionally and based on input value in a Play 2 controller in Scala and ReactiveMongo

I've a Play controller Action that edits a document in MongoDB using ReactiveMongo. The code is shown below. Both name and keywords are optional. I'm creating a temp BSONDocument() and adding tuples to it based on if name and keywords exist are not empty. However, tmp is currently mutable(is a var). I'm wondering how I can get rid of the var.
def editEntity(id: String, name: Option[String], keywords: Option[String]) = Action {
val objectId = new BSONObjectID(id)
//TODO get rid of var here
var tmp = BSONDocument()
if (name.exists(_.trim.nonEmpty)) {
tmp = tmp.add(("name" -> BSONString(name.get)))
}
val typedKeywords : Option[List[String]] = Utils.getKeywords(keywords)
if (typedKeywords.exists(_.size > 0)) {
tmp = tmp.add(("keywords" -> typedKeywords.get.map(x => BSONString(x))))
}
val modifier = BSONDocument("$set" -> tmp)
val updateFuture = collection.update(BSONDocument("_id" -> objectId), modifier)
}
UPDATE After looking at the solution from #Vikas it came to me what if there are more (say 10 or 15) number of input Options that I need to deal with. Maybe a fold or reduce based solution will scale better?
In your current code you're adding an empty BSONDocument() if none of those if conditions matched? val modifier = BSONDocument("$set" -> tmp) will have an empty tmp if name was None and typedKeyWords was None. Assuming that's what you want here is one approach to get rid of transient var. also note having a var locally (in a method) isn't a bad thing (sure I'll still make that code look bit prettier)
val typedKeywords : Option[List[String]] = Utils.getKeywords(keywords)
val bsonDoc = (name,typedKeywords) match{
case (Some(n),Some(kw) ) => BSONDocument().add( "name" -> BSONString(n)) .add(("keywords" -> kw.map(x => BSONString(x))))
case (Some(n), None) => BSONDocument().add( "name" -> BSONString(n))
case (None,Some(kw)) => BSONDocument().add(("keywords" -> kw.map(x => BSONString(x))))
case (None,None) => BSONDocument()
}
val modifier = BSONDocument("$set" -> bsonDoc)

How to get a value as number from Mongodb, casbah

I have a simple code for getting the port number from MongoDB. I use scala and the driver is of course casbah.
def getPortNo : Int {
val query = MongoDBObject("_id" -> "Store")
val data = coll.findOne(query)
return data.get("port")
}
Here my application only has one document that id is equal to "store".
but this is not resolved in IDE.
I have the same code for getting the version.
def getVersion : String = {
val query = MongoDBObject("_id" -> "Store")
val data = coll.findOne(query)
return data.get("version").toString
}
this works well.
I tried data.get("port").toString.toInt and It also does not work.
Can someone tell me how to do this. I think the problem here is the returning value in not either number or a string. what is the return type and how can I cast it into a number.
It depends on how you store "port" field. Try data.as[Number]("value").intValue(). It must work any number format.
And you should consider that findOne returns Option, so you can return Option too:
def getPortNo : Option[Int] = {
val query = MongoDBObject("_id" -> "Store")
val data = coll.findOne(query)
data.map(_.as[Number]("port").intValue)
}
Or use some default value:
def getPortNo : Int = {
val query = MongoDBObject("_id" -> "Store")
val data = coll.findOne(query)
data.map(_.as[Number]("port").intValue).getOrElse(80)
}

How to query with '$in' over '_id' in reactive mongo and play

I have a project set up with playframework 2.2.0 and play2-reactivemongo 0.10.0-SNAPSHOT. I'd like to query for few documents by their ids, in a fashion similar to this:
def usersCollection = db.collection[JSONCollection]("users")
val ids: List[String] = /* fetched from somewhere else */
val query = ??
val users = usersCollection.find(query).cursor[User].collect[List]()
As a query I tried:
Json.obj("_id" -> Json.obj("$in" -> ids)) // 1
Json.obj("_id.$oid" -> Json.obj("$in" -> ids)) // 2
Json.obj("_id" -> Json.obj("$oid" -> Json.obj("$in" -> ids))) // 3
for which first and second return empty lists and the third fails with error assertion 10068 invalid operator: $oid.
NOTE: copy of my response on the ReactiveMongo mailing list.
First, sorry for the delay of my answer, I may have missed your question.
Play-ReactiveMongo cannot guess on its own that the values of a Json array are ObjectIds. That's why you have to make a Json object for each id that looks like this: {"$oid": "526fda0f9205b10c00c82e34"}. When the ReactiveMongo Play plugin sees an object which first field is $oid, it treats it as an ObjectId so that the driver can send the right type for this value (BSONObjectID in this case.)
This is a more general problem actually: the JSON format does not match exactly the BSON one. That's the case for numeric types (BSONInteger, BSONLong, BSONDouble), BSONRegex, BSONDateTime, and BSONObjectID. You may find more detailed information in the MongoDB documentation: http://docs.mongodb.org/manual/reference/mongodb-extended-json/ .
I managed to solve it with:
val objectIds = ids.map(id => Json.obj("$oid" -> id))
val query = Json.obj("_id" -> Json.obj("$in" -> objectIds))
usersCollection.find(query).cursor[User].collect[List]()
since play-reactivemongo format considers BSONObjectID only when "$oid" is followed by string
implicit object BSONObjectIDFormat extends PartialFormat[BSONObjectID] {
def partialReads: PartialFunction[JsValue, JsResult[BSONObjectID]] = {
case JsObject(("$oid", JsString(v)) +: Nil) => JsSuccess(BSONObjectID(v))
}
val partialWrites: PartialFunction[BSONValue, JsValue] = {
case oid: BSONObjectID => Json.obj("$oid" -> oid.stringify)
}
}
Still, I hope there is a cleaner solution. If not, I guess it makes it a nice pull request.
I'm wondering if transforming id to BSONObjectID isn't more secure this way :
val ids: List[String] = ???
val bsonObjectIds = ids.map(BSONObjectID.parse(_)).collect{case Success(t) => t}
this will only generate valid BSONObjectIDs (and discard invalid ones)
If you do it this way :
val objectIds = ids.map(id => Json.obj("$oid" -> id))
your objectIds may not be valid ones depending on string id really being the stringify version of a BSONObjectID or not
If you import play.modules.reactivemongo.json._ it work without any $oid formatters.
import play.modules.reactivemongo.json._
...
val ids: Seq[BSONObjectID] = ???
val selector = Json.obj("_id" -> Json.obj("$in" -> ids))
usersCollection.find(selector).cursor[User].collect[Seq]()
I tried with the following and it worked for me:
val listOfItems = BSONArray(51, 61)
val query = BSONDocument("_id" -> BSONDocument("$in" -> listOfItems))
val ruleListFuture = bsonFutureColl.flatMap(_.find(query, Option.empty[BSONDocument]).cursor[ResponseAccDataBean]().
collect[List](-1, Cursor.FailOnError[List[ResponseAccDataBean]]()))

Getting a reference to an immutable Map

I'm parallelising over a collection to count the number same item values in a List. The list in this case is uniqueSetOfLinks :
for (iListVal <- uniqueSetOfLinks.par) {
try {
val num : Int = listOfLinks.count(_.equalsIgnoreCase(iListVal))
linkTotals + iListVal -> num
}
catch {
case e : Exception => {
e.printStackTrace()
}
}
}
linkTotals is an immutable Map. To gain a reference to the total number of links do I need to update linkTotals so that it is immutable ?
I can then do something like :
linkTotals.put(iListVal, num)
You can't update immutable collection, all you can do is to combine immutable collection with addition element to get new immutable collection, like this:
val newLinkTotals = linkTotals + (iListVal -> num)
In case of collection you could create new collection of pairs and than add all pairs to the map:
val optPairs =
for (iListVal <- uniqueSetOfLinks.par)
yield
try {
val num : Int = listOfLinks.count(_.equalsIgnoreCase(iListVal))
Some(iListVal -> num)
}
catch {
case e : Exception => e.printStackTrace()
None
}
val newLinkTotals = linkTotals ++ optPairs.flatten // for non-empty initial map
val map = optPairs.flatten.toMap // in case there is no initial map
Note that you are using parallel collections (.par), so you should not use mutable state, like linkTotals += iListVal -> num.
Possible variation of #senia's answer (got rid of explicit flatten):
val optPairs =
(for {
iListVal <- uniqueSetOfLinks.par
count <- {
try
Some(listOfLinks.count(_.equalsIgnoreCase(iListVal)))
catch {
case e: Exception =>
e.printStackTrace()
None
}
}
} yield iListVal -> count) toMap
I think that you need some form of MapReduce in order to have parallel number of items estimation.
In your problem you already have all unique links. The partial intermediate result of map is simply a pair. And "reduce" is just toMap. So you can simply par-map the link to pair (link-> count) and then finally construct a map:
def count(iListVal:String) = listOfLinks.count(_.equalsIgnoreCase(iListVal))
val listOfPairs = uniqueSetOfLinks.par.map(iListVal => Try( (iListVal, count(iListVal)) ))
("map" operation is par-map)
Then remove exceptions:
val clearListOfPairs = listOfPairs.flatMap(_.toOption)
And then simply convert it to a map ("reduce"):
val linkTotals = clearListOfPairs.toMap
(if you need to check for exceptions, use Try.failure)