real polymorphism in embed relations in mongoid

2020-07-22 18:17发布

问题:

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.

回答1:

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!


回答2:

Well, it seems that's not possible yet in Mongoid github.com/mongoid/mongoid/issues/2560 . So, I do something like that:

class Item
    include Mongoid::Document
    embeds_one :picture
    embeds_one :sticker
    embeds_one :calendar

    # attributes to the right field the document to embed
    def detail=(my_object)
        send("#{my_object.class.to_s.downcase}=", my_object)
        detail
    end

    # returns the embedded object
    def detail
        picture || sticker || calendar
    end
end

# Similar to Sticker and Calendar
class Picture
    embedded_in :item
end

If someone has a better answer, I take it !
howsoever, hope it will help the next one in my case.