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.