What is a good way to `update_or_initialize_with` in Mongoid?

1.1k Views Asked by At

Each user has one address.

class User
  include Mongoid::Document

  has_one :address
end

class Address
  include Mongoid::Document

  belongs_to :user

  field :street_name, type:String
end

u = User.find(...)
u.address.update(street_name: 'Main St')

If we have a User without an Address, this will fail.

So, is there a good (built-in) way to do u.address.update_or_initialize_with?

Mongoid 5

2

There are 2 best solutions below

0
vdj4y On

I am not familiar with ruby. But I think I understand the problem. Your schema might looks like this.

user = {
   _id : user1234,
   address: address789
} 

address = {
   _id: address789,
   street_name: ""
   user: user1234
}

//in mongodb(javascript), you can get/update address of user this way
u = User.find({_id: user1234})
u.address //address789
db.address.update({user: u.address}, {street_name: "new_street name"})

//but since the address has not been created, the variable u does not even have property address.
u.address = undefined

Perhaps you can try to just create and attached it manually like this:

 #create an address document, to get _id of this address
 address = address.insert({street_name: "something"}); 

 #link or attached it to u.address
 u.update({address: address._id})
1
Shiyason On

I had this problem recently. There is a built in way but it differs from active records' #find_or_initialize_by or #find_or_create_by method.

In my case, I needed to bulk insert records and update or create if not found, but I believe the same technique can be used even if you are not bulk inserting.

# returns an array of query hashes:
def update_command(users)
  updates = []
  users.each do |user|
    updates << { 'q' => {'user_id' => user._id},
                 'u' => {'address' => 'address'},
                 'multi' => false,
                 'upsert' => true }
  end
  { update: Address.collection_name.to_s, updates: updates, ordered: false }
end

def bulk_update(users)
  client = Mongoid.default_client
  command = bulk_command(users)
  client.command command
  client.close
end

since your not bulk updating, assuming you have a foreign key field called user_id in your Address collection. You might be able to:

Address.collection.update({ 'q' => {'user_id' => user._id},
                            'u' => {'address' => 'address'},
                            'multi' => false,
                            'upsert' => true }

which will match against the user_id, update the given fields when found (address in this case) or create a new one when not found.

For this to work, there is 1 last crucial step though. You must add an index to your Address collection with a special flag.

The field you are querying on (user_id in this case) must be indexed with a flag of either { unique: true } or { sparse: true }. the unique flag will raise an error if you have 2 or more nil user_id fields. The sparse option wont. Use that if you think you may have nil values.

access your mongo db through the terminal

  1. show dbs
  2. use your_db_name check if the addresses collection already has the index you are looking for
  3. db.addresses.getIndexes() if it already has an index on user_id, you may want to remove it db.addresses.dropIndex( { user_id: 1} ) and create it again with the following flag:
  4. db.addresses.createIndex( { user_id: 1}, { sparse: true } )

https://docs.mongodb.com/manual/reference/method/db.collection.update/

EDIT #1

There seems to have changes in Mongoid 5.. instead of User.collection.update you can use User.collection.update_one

https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/

The docs show you need a filter rather than a query as first argument but they seem to be the same..

Address.collection.update_one( { user_id: user_id },
  '$set' => { "address": 'the_address', upsert: true} )

PS: If you only write { "address": 'the_address' } as your update clause without including an update operator such as $set, the whole document will get overwritten rather than updating just the address field.

EDIT#2

About why you may want to index with unique or sparse

If you look at the upsert section in the link bellow, you will see:

To avoid multiple upserts, ensure that the filter fields are uniquely indexed.

https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/