Search code examples
rubymongoidpolymorphic-associations

real polymorphism in embed relations in mongoid


Using Mongoid3, I'm trying to add polymorphism to my embedded relations.

I have a class Item that must embed_one object containing my informations. The deal is that :
- my object's type can be one of those : Calendar, Sticker, Picture;
- regardless my object's type, I want to access it by a unique 'key' : detail
e.g. :

pry> my_item1.detail  
=> `<Picture _id: 1234>`  
pry> my_item2.detail  
=> `<Sticker _id: 8964>`
pry>

First, I tried using the keywords as and polymorphic like described here: https://github.com/mongoid/mongoid/issues/902
e.g. :

class Item
    include Mongoid::Document
    embeds_one  :detail, as: :item_slot
end

class Picture
    include Mongoid::Document
    embedded_in :item_slot, polymorphic: true
end

class Calendar
    include Mongoid::Document
    embedded_in :item_slot, polymorphic: true
end

class Sticker
    include Mongoid::Document
    embedded_in :item_slot, polymorphic: true
end

Then I'm trying to access to my detail but unhappily, I got this message error :

pry(main)> i = Item.new
pry(main)> i.detail
=> nil
pry(main)> i.detail = Picture.find('50b864').dup
pry(main)> i.save
=> true
pry(main)> i = Item.find i._id
pry(main)> i.detail
NameError: uninitialized constant Detail
from /home/jg/.rvm/gems/ruby-1.9.3-p448/gems/activesupport-3.2.14/lib/active_support/inflector/methods.rb:230:in `block in constantize'

It says mongoid didn't find any .detail into my item. Why not.

Then, I found this topic mongoid polymorphic association error telling to add class_name. So I updated my code like so :

class Item  
    include Mongoid::Document
    embeds_one  :detail, class_name: "Picture", class_name: "Calendar", class_name: "Sticker"
end

Let's try:

pry(main)> i.detail = Picture.find('50b864').dup
pry(main)> i.save
=> true
pry(main)> i = Item.find i._id
pry(main)> i.detail
=> #<Sticker _id: 52961d>

That's embarrassing because I was expecting to get a Picture, not a Sticker (it has my picture's values inside). (The result is the same if I put each different class_name value on new lines.)

For my third try, I used Custom Relation Names like so :

class Item  
    include Mongoid::Document
    embeds_one  :detail, class_name: "Picture", class_name: "Calendar", class_name: "Sticker", inverse_of: :item_slot
end

class Picture # for Calendar and Sticker too ofc
    include Mongoid::Document
    embedded_in :item_slot, polymorphic: true, inverse_of: :detail
end  

But it gave me a Sticker too.
I even tried :

class Item  
    include Mongoid::Document
    embeds_one  :detail, inverse_of: :item_slot
end  

But it send me again the NameError seen previously.

My last try was to use inheritance :

class Item  
    include Mongoid::Document
    embeds_one :detail
end

class Detail
    include Mongoid::Document
    embedded_in :item
end

# Same again for Calendar and Sticker
class Picture < Detail
    ... # regular other fields
end  

But it gives me awful messages when lunching pry for sticker.rb and calendar.rb

 DEVEL -  Failed to load .../models/sticker.rb; removing partially defined constants
 DEVEL -  Problem while loading .../models/sticker.rb: uninitialized constant Detail  

I have no more idea..
==> Have anyone a tips ?

EDIT :
It would be nice to have an equivalent to Hash[e.attributes] like here Extract `Moped::BSON::Document` attributes in Ruby hash and then do like so :

class Item
    include Mongoid::Document
    field :detail
end  

that keep my Picture, Calendar and Sticker as class instances (because I have different methods for each to apply after saving).

JG

EDIT: Alternative way
edit 08/08/2014, split that part as the answer of that topic.


Solution

  • You should use inheritance both with polymorphic relation Here you go:

    base class

    class Resource
      include Mongoid::Document
      include Mongoid::Timestamps
    
      embedded_in :resoursable, polymorphic: true
    end
    

    childs

    class Photo < Resource
      field :width,  type: Integer
      field :height,  type: Integer
    end
    
    class Video < Resource
      field :url,  type: String
    end
    

    embedding

    class Post
      include Mongoid::Document
      include Mongoid::Timestamps
    
      embeds_one :media, as: :resoursable, class_name: 'Resource'
    end
    

    code

    p = Post.last
    resource = Photo.new
    p.media = resource
    p.save!