B Seven
B Seven

Reputation: 45941

How to prepend an array and remove dupes in Mongo?

In MongoDB, what is a good way to prepend an item to an array, and remove any duplicate items?

$addToSet does not do anything if the item is already in the array.

This SO question shows two ways of prepending, but does not remove dupes.

Working in Mongo 2.4.1, MongoMapper and Ruby.

Upvotes: 1

Views: 428

Answers (2)

Neil Lunn
Neil Lunn

Reputation: 151122

The $addToSet operator will not of course remove existing duplicates, nor as being a "set" are any of the elements considered to be ordered so there is no way to position them with this operator.

In either case, looking after duplicates by your logic is now up to you to manage. But rather than pulling the document and manipulating in code, which could cause you concurrency problems, the bulk operations API can help you.

So in order to ensure your logic you would use $pull to remove any items from the array that you were inserting, followed by a $push, using the $position modifier that needs to be coupled with the $each modifier.

A basic mongomapper code example:

require 'mongo_mapper'
require 'pp'

MongoMapper.database = 'test'

class User
  include MongoMapper::Document

  key :array, Array
end
User.collection.remove

user = User.create(:array => [5, 4, 4, 6])

pp user

bulk = User.collection.initialize_unordered_bulk_op

bulk.find(:_id => user._id, :array => 4)
  .update_one({ "$pull" => { "array" => 4 }})

bulk.find({"_id" => user._id, "array" => { "$ne" => 4 }})
  .update_one({
    "$push" => {
      "array" => { "$each" => [4], "$position" => 0 }
    }
  })

res = bulk.execute()

pp res

pp user.reload

Strictly speaking, that is still not an atomic operation and is performed in multiple updates. But the API implementation does send both of these requests over the wire together as well as implements them in succession. So this is as close to an atomic operation as you are currently going to get given your logic.

The resulting array in this example is of course:

array: [4, 5, 6]

Upvotes: 1

JohnnyHK
JohnnyHK

Reputation: 312055

You can do this by qualifying your update conditions to include that the value to prepend isn't already in the array. Then by using $push with $each and $position you can insert the item at the beginning of the array, only if it's not already present in the array.

Assuming a doc that looks like:

{
  "_id": 1,
  "a": [1]
}

You can perform the update in the shell as:

var value = 1;
db.test.update(
    {_id: 1, a: {$ne: value}}, 
    {$push: {a: {$each: [value], $position: 0}}})

Which for a value of 1 has no effect, but any other value would be prepended to the a array field.

Upvotes: 1

Related Questions