Mongoid: Polymorphic Find Or Create New On Embedded Document Collections


In the old v2.0.beta.20 version of Mongoid, I was able to call .find_or_create_by on an embedded document collection and pass a type as a second parameter to the method. This would allow me to create a document of a specific type when I needed to, instead of creating a document of the type specified in the original relationship.

For example, given this entity structure:

class Assessment
  include Mongoid::Document

  embeds_many :questions
end

class Question
  include Mongoid::Document
  
  embedded_in :assessment

  field :name
end

class YesNoQuestion < Question
  field :yes, :type => Boolean
end

I would be able to call this:

assessment.questions.find_or_create_by({:name => "Some name"}, YesNoQuestion)

and it would create the YesNoQuestion type and add it to the assesment.questions collection, instead creating the base question type.

Rolling My Own

Well, we upgraded to v2.0.1 recently, and the .find_or_create_by method signature has changed. It no longer supports creation of a specified type. In fact, it doesn’t want me to pass a second parameter to the method at all. But I need to be able to do this in the same way, for the same reasons, as I was doing it with beta.20. I asked on the mailing list and on stackoverflow, but didn’t get a good answer, so I rolled my own.

The good news is the source code for Mongoid is fairly easy to follow. I figured out that there is a module the Mongoid::Relations namespaced called Many and that this module is included in all embeds_many relationships. With that in mind, I added my own method to it (being sure not to accidentally monkey-patch any existing methods) to find or add-new with a specified type.

module Mongoid::Relations
  class Many
    def find_or_new(attrs, type, &block)
      inst = self.where(attrs).first

      unless inst
        inst = type.new
        inst.write_attributes attrs
        self << inst
      end

      inst
    end
  end
end

Now I can call this on my model:

assessment.questions.find_or_new({:name => "Some name"}, YesNoQuestion)

and it works the way I need it to work.

Note that I specifically called this find_or_new for several reasons. This is no longer a find_or_create_by, because I’m not calling “create”. Calling create on the Question or YesNoQuestion object directly throws an exception because it’s an embedded document. Also, find_or_initialize_by already exists and like find_or_create_by, it does not let me specify the type to initialize.

Don’t Worry About Where To Start. Just Start.