How to enforce unique embedded document in mongoid

2019-06-16 17:50发布

问题:

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})

回答1:

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})



回答2:

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

validates :name, :uniqueness => true

That should ensure uniqueness within parent document.



回答3:

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.



回答4:

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.



回答5:

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).



回答6:

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


回答7:

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)