1

I have a model band that contains a list of tours

Band:
{ 
  name: String,
  email: String,
  createdAt: String,
  tours: Tour[],
  ...
}

where a Tour is:

{
name: String,
region: String,
published: Boolean,
...
}

The goal is simply to create an end point that receives a Band Name and Tour Name deletes a tour based on this input.

The following works:

bandService.getBandByName(req.getParam("bandName")).flatMap{ b =>
  val tour = b.tours.filter(t => t.name == req.getParam("tourName")).head
  mongoDataBaseConnector.bands.findOneAndUpdate(
    equal("bandName", req.getParam("bandName")),
    pull("tours", tour)
  ).toFuture().flatMap(u => bandService.getBandByName(req.getParam("bandName")))

However, this requires me to first resolve the band by the name received first, filter, find the tour and pass in the exact object in to the pull I am trying to avoid this by using pullByFilter but can't seem to get this to work. Unfortunately couldn't find any examples of this function in the scala driver.

This is what I am trying:

mongoDataBaseConnector.bands.findOneAndUpdate(
      and(
        equal("bandName", req.getParam("bandName")),
        equal("tours.name", req.getParam("tourName"))),
      pullByFilter(and(
        equal("tours.$.name", req.getParam("tourName")),
        equal("tours.$.region", req.getParam("region"))
      ))
    ).toFuture().flatMap(u => bandService.getBandByName(req.getParam("bandName")))

this gives the following error:

com.mongodb.MongoCommandException: Command failed with error 2 (BadValue): 'Cannot apply $pull to a non-array value' on server cluster0-shard-00-01-sqs4t.mongodb.net:27017. The full response is {"operationTime": {"$timestamp": {"t": 1568589476, "i": 8}}, "ok": 0.0, "errmsg": "Cannot apply $pull to a non-array value", "code": 2, "codeName": "BadValue", "$clusterTime": {"clusterTime": {"$timestamp": {"t": 1568589476, "i": 8}}, "signature": {"hash": {"$binary": "Qz/DqAdG11H8KRkW8gtvRAAE61Q=", "$type": "00"}, "keyId": {"$numberLong": "6710077417139994625"}}}}

Any ideas are appreciated. Is this even possible with this function?

sinanspd
  • 2,589
  • 3
  • 19
  • 37
  • Have you first tried the query in mongoshell? Before programmatically? To check query syntax/grammar? – cchantep Sep 16 '19 at 08:46
  • @cchanted I am not sure what you mean. I believe pullByFilter is a function defined in the Java/Scala DSLs, I can't seem to find any reference to it in the shell docs. It likely evaluates to {..$filter(...), $pull(...)}. As I have mentioned in the post, I am able to get this to work using pull, granted I resolve the target object first (which I am trying to avoid). This asserts that the field names etc. are indeed correct. The problem is either in the DSL synthax I am using or the DSL function itself is not capable of evaluating this kind of query – sinanspd Sep 17 '19 at 04:16
  • DSL is just a way to write BSON, if it works in shell, then you are able to write the corresponding BSON – cchantep Sep 17 '19 at 07:37
  • Yes I can just write the string query and execute but the question is if this can be achieved with any built in DSL functions. I am still not able to get pullByFilter to work so I am guessing it wasn't built for this purpose, the documentation is not clear. – sinanspd Sep 17 '19 at 17:49

1 Answers1

0

Okay, I've been working on a nearly identical problem all day. What I have isn't great, but it's enough that I'm willing to accept it and hopefully someone will be able to make it into a more fully-formed solution. Or at least I can save someone else some time.

pullByFilter only accepts one argument. It seems that argument can be another Bson filter which is applied directly to each of the children, or a BsonValue that will be matched against. Since none of the Bson filters step into the child documents, you have to create a BsonValue, specifically, a document. I believe this should solve your problem:

mongoDataBaseConnector.bands.findOneAndUpdate(
      and(
        equal("bandName", req.getParam("bandName")),
        equal("tours.name", req.getParam("tourName"))),
      pullByFilter(BsonDocument().append("tours", BsonDocument()
          .append("name", req.getParam("tourName"))
          .append("region", req.getParam("region"))
      ))
    ).toFuture().flatMap(u => bandService.getBandByName(req.getParam("bandName")))

I would have liked to be able to switch back using Bson filters after getting to the inner document, but it seems that once you're in BsonValues, you can't go back.

Edit: To help whoever comes after me: I think there should be a way to solve this more cleanly with arrayFilters, but I haven't yet found it.

Thomas
  • 871
  • 2
  • 8
  • 21