opsb
opsb

Reputation: 30231

How to enforce unique embedded document in mongoid

I have the following model

class Person 
  include Mongoid::Document
  embeds_many :tasks
end

class Task
  include Mongoid::Document
  embedded_in :commit, :inverse_of => :tasks
  field :name
end

How can I ensure the following?

person.tasks.create :name => "create facebook killer"
person.tasks.create :name => "create facebook killer"

person.tasks.count == 1

different_person.tasks.create :name => "create facebook killer"
person.tasks.count == 1
different_person.tasks.count == 1

i.e. task names are unique within a particular person


Having checked out the docs on indexes I thought the following might work:

class Person 
  include Mongoid::Document
  embeds_many :tasks

  index [
      ["tasks.name", Mongo::ASCENDING], 
      ["_id", Mongo::ASCENDING]
  ], :unique => true
end

but

person.tasks.create :name => "create facebook killer"
person.tasks.create :name => "create facebook killer"

still produces a duplicate.


The index config shown above in Person would translate into for mongodb

db.things.ensureIndex({firstname : 1, 'tasks.name' : 1}, {unique : true})

Upvotes: 9

Views: 5727

Answers (7)

Leopd
Leopd

Reputation: 42769

You can also specify the index in your model class:

index({ 'firstname' => 1, 'tasks.name' => 1}, {unique : true, drop_dups: true })

and use the rake task

rake db:mongoid:create_indexes

Upvotes: 0

Mark
Mark

Reputation: 1068

You can define a validates_uniqueness_of on your Task model to ensure this, according to the Mongoid documentation at http://mongoid.org/docs/validation.html this validation applies to the scope of the parent document and should do what you want.

Your index technique should work too, but you have to generate the indexes before they brought into effect. With Rails you can do this with a rake task (in the current version of Mongoid its called db:mongoid:create_indexes). Note that you won't get errors when saving something that violates the index constraint because Mongoid (see http://mongoid.org/docs/persistence/safe_mode.html for more information).

Upvotes: 0

BenG
BenG

Reputation: 1766

you have to run :

db.things.ensureIndex({firstname : 1, 'tasks.name' : 1}, {unique : true})

directly on the database

You appear to including a "create index command" inside of your "active record"(i.e. class Person)

Upvotes: -1

Gurpartap Singh
Gurpartap Singh

Reputation: 2764

Add a validation check, comparing the count of array of embedded tasks' IDs, with the count of another array with unique IDs from the same.

validates_each :tasks do |record, attr, tasks|
  ids = tasks.map { |t| t._id }
  record.errors.add :tasks, "Cannot have the same task more than once." unless ids.count == ids.uniq.count
end

Worked for me.

Upvotes: 0

Andy Agrawal
Andy Agrawal

Reputation: 79

I don't believe this is possible with embedded documents. I ran into the same issue as you and the only workaround I found was to use a referenced document, instead of an embedded document and then create a compound index on the referenced document.

Obviously, a uniqueness validation isn't enough as it doesn't guard against race conditions. Another problem I faced with unique indexes was that mongoid's default behavior is to not raise any errors if validation passes and the database refuses to accept the document. I had to change the following configuration option in mongoid.yml:

persist_in_safe_mode: true

This is documented at http://mongoid.org/docs/installation/configuration.html

Finally, after making this change, the save/create methods will start throwing an error if the database refuses to store the document. So, you'll need something like this to be able to tell users about what happened:

alias_method :explosive_save, :save

def save
  begin
    explosive_save
  rescue Exception => e
    logger.warn("Unable to save record: #{self.to_yaml}. Error: #{e}")
    errors[:base] << "Please correct the errors in your form"
    false
  end
end

Even this isn't really a great option because you're left guessing as to which fields really caused the error (and why). A better solution would be to look inside MongoidError and create a proper error message accordingly. The above suited my application, so I didn't go that far.

Upvotes: 0

Paul Elliott
Paul Elliott

Reputation: 1184

Can't you just put a validator on the Task?

validates :name, :uniqueness => true

That should ensure uniqueness within parent document.

Upvotes: 5

Gates VP
Gates VP

Reputation: 45307

Indexes are not unique by default. If you look at the Mongo Docs on this, uniqueness is an extra flag.

I don't know the exact Mongoid translation, but you're looking for something like this:

db.things.ensureIndex({firstname : 1}, {unique : true, dropDups : true})

Upvotes: 1

Related Questions