4

I have seen that this question seems to have been asked earlier (almost 3 years ago), but since then there might be lot of changes in the reactive mongo library.

I am using the play plugin with version 2.4, but the reactivemongo.api.commands.WriteResult does not seem to have any API to get the document object id.

Now I can start setting the object id by myself, but I do not find it a convincing and right idea since some value unique on the machine where I create the id might not be same with the other machine and to keep things simple I want to let this being handled by mongo db.

So yeah if there is some way that I can get the id of the inserted document will be great or else I have to fallback to the way to setting the id by myself which I something I am trying to avoid.

Gaurav Abbi
  • 645
  • 9
  • 23
  • you can do `insertResult.map(_.result[Person])` or whatever you are using, even `BSONDocument` and get the id from it. Found in docs here: http://reactivemongo.org/releases/0.11/documentation/tutorial/write-documents.html – Łukasz Mar 23 '16 at 13:39
  • That's a design issue. Expecting the DB to generate ID lead to such problem. Better to use `BSONObjectID.generate` (or any other UUID generator). – cchantep Mar 23 '16 at 14:58

2 Answers2

4

There are at least two proper ways to solve this. Actually, the second one is more of a hack, but it works properly. I've created a gist with a complete example (tested).

Generate BSONObjectId on the client

BSONObjectId has a method called generate which you can use to easily generate a unique ID. The resulting ID has the same structure as IDs generated by MongoDB server itself:

+------------------------+------------------------+------------------------+------------------------+
+ timestamp (in seconds) +   machine identifier   +    thread identifier   +        increment       +
+        (4 bytes)       +        (3 bytes)       +        (2 bytes)       +        (3 bytes)       +
+------------------------+------------------------+------------------------+------------------------+

(taken from ReactiveMongo's javadoc for BSONObjectId.generate)

Example code:

val id = BSONObjectId.generate()
collection.insert(BSONDocument("_id" -> id, "foo" -> "bar"))

The resulting ID won't be exactly the same as generated by the server. Specifically, machine identifier and thread identifier will be totally different, most likely. However, in most (all?) cases it won't matter, because the risk of collision is negligible, so in my opinion this is the best approach.

You should also check the value returned by insert. If it fails, you can generate a new ID and try inserting again. This way your code should be bullet-proof. Remember to limit the number of retries and maybe add some random delay between each try.

Use BSONCollection.findAndUpdate

If you really need to generate IDs on the server side for some obscure reason, this solution should do that, avoiding race conditions or any additional queries, so it's quite optimal, albeit somewhat hacky. The trick is to use MongoDB's findAndModify. This way, you'll get FindAndModifyResult instead of WriteResult, which gives you access to the inserted document. Example:

val resultFuture = collection.findAndUpdate(
    selector       = BSONDocument("foo" -> -1),    // Assuming that there's no document with "foo" equal to -1, otherwise THIS CODE MAY EXPLODE
    update         = BSONDocument("foo" -> "bar"),
    fetchNewObject = true,
    upsert         = true)
val idFuture: Future[BSONObjectId] = resultFuture.map { result =>
    val doc = result.value.get // WARNING: I'm using get for simplicity, but you shouldn't: it may throw an exception
    doc.getAs[BSONObjectId]("_id"))
}

Please note that you must provide a selector which never selects a document, otherwise this would update an existing document instead of inserting a new one. Moreover, you need to override these fields in update, otherwise it would use the values from the selector.

Bonus: Use a separate collection for incremental IDs

You can also create an additional collection for keeping the most recent ID as described in MongoDB's documentation. This should be safe as long as you're using findAndModify to simultaneously get a new ID and increment the value. Drawbacks? One additional collection and one additional query, but there are some cases where you need auto-incremented unique IDs anyway, so it may be worth consideration.

  • Hi, this is my first post on StackOverflow. Please let me know if you think I could've done something better. – Paweł Bartkiewicz Nov 18 '16 at 01:48
  • Interesting use of findAndUpdate, unfortunate that you need to provide a non-matching selector, I probably wouldn't recommend using that approach in a production app.. – qwwqwwq Mar 02 '17 at 16:38
  • Yes, **I'd never use it** myself, but it's still better than e.g. inserting an object and querying DB for the highest ID (race condition). Who knows, maybe someone has a valid reason for generating IDs server-side, so I'll keep this solution. – Paweł Bartkiewicz Mar 16 '17 at 13:12
1

Creating ObjectId on the client side is fine. It's what casbah (a blocking MongoDB driver for MongoDB) is doing if you dig into its code.

If you take a look at ObjectId fields, then you'll find a 3-byte machine identifier among others. It's calculated tricky (involving InetAddress in standard Java driver). It guarantees that there cannot be a collision between 2 ObjectIds, generated at the same time on different machines.

Roman
  • 64,384
  • 92
  • 238
  • 332