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.