Question

I'm having trouble updating an embedded document in a Ruby / Sinatra app. I'm trying to use the positional operator "$" in an update statement in order to select the correct document from the embedded array. But this is throwing an "ArgumentError - wrong number of arguments (2 for 1)" error.

A simple update statement with a hard-coded array index works fine. So maybe Mongoid/Moped doesn't support the position operator?... Although from what I could see, it looks like it should.

Does anyone know what the best approach is? Is there some other way to determine sub-document index without iterating through them all using Ruby in my controller - which is plan B, but seems really flaky!...

Here is my basic set up: I have "Customers"...

class Customer
    include Mongoid::Document
    field :customer_name, type: String

    embeds_many :contacts

    attr_accessible :customer_name
end

... with embedded "Contacts"...

class Contact
    include Mongoid::Document
    field :first_name, type: String

    attr_accessible :first_name

    embedded_in :customer
end

And in my controller I get the ._ids of both customer (pk), and the specific embedded document to update (contact_pk):

Customer.update(
                 { 
                   "_id" => Moped::BSON::ObjectId(pk),"contacts._id" => Moped::BSON::ObjectId(contact_pk)
                 },
                 {
                   $set => {"contacts.$.first_name" => "Bob" }
                 }
                ) 
Was it helpful?

Solution

The update class method is actually Customer.with_default_scope.update in disguise, that means that update is actually the update method from Mongoid::Criteria and that looks like this:

# Update the first matching document atomically.
#
# @example Update the first matching document.
#   context.update({ "$set" => { name: "Smiths" }})
#
# @param [ Hash ] attributes The new attributes for the document.
#
# @return [ nil, false ] False if no attributes were provided.
#
# @since 3.0.0
def update(attributes = nil)
  update_documents(attributes)
end

Notice that it only takes a single attributes parameter? That explains the error message. Presumably you're expecting update to work the same way as it does in the MongoDB shell or the JavaScript interface.

First you need to find the document of interest and then call update on that:

Customer.where('_id' => Moped::BSON::ObjectId(pk), 'contacts._id' => Moped::BSON::ObjectId(contact_pk))
        .update($set => { 'contacts.$.first_name' => 'Bob' })
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top