Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Association for polymorphic belongs_to of a particular type

I'm relatively new to Rails. I would like to add an association to a model that uses the polymorphic association, but returns only models of a particular type, e.g.:

class Note < ActiveRecord::Base
  # The true polymorphic association
  belongs_to :subject, polymorphic: true

  # Same as subject but where subject_type is 'Volunteer'
  belongs_to :volunteer, source_association: :subject
  # Same as subject but where subject_type is 'Participation'
  belongs_to :participation, source_association: :subject
end

I've tried a vast array of combinations from reading about the associations on ApiDock but nothing seems to do exactly what I want. Here's the best I have so far:

class Note < ActiveRecord::Base
  belongs_to :subject, polymorphic: true
  belongs_to :volunteer, class_name: "Volunteer", foreign_key: :subject_id, conditions: {notes: {subject_type: "Volunteer"}}
  belongs_to :participation, class_name: "Participation", foreign_key: :subject_id, conditions: {notes: {subject_type: "Participation"}}
end

And I want it to pass this test:

describe Note do
  context 'on volunteer' do
    let!(:volunteer) { create(:volunteer) }
    let!(:note) { create(:note, subject: volunteer) }
    let!(:unrelated_note) { create(:note) }

    it 'narrows note scope to volunteer' do
      scoped = Note.scoped
      scoped = scoped.joins(:volunteer).where(volunteers: {id: volunteer.id})
      expect(scoped.count).to be 1
      expect(scoped.first.id).to eq note.id
    end

    it 'allows access to the volunteer' do
      expect(note.volunteer).to eq volunteer
    end

    it 'does not return participation' do
      expect(note.participation).to be_nil
    end

  end
end

The first test passes, but you can't call the relation directly:

  1) Note on volunteer allows access to the volunteer
     Failure/Error: expect(note.reload.volunteer).to eq volunteer
     ActiveRecord::StatementInvalid:
       PG::Error: ERROR:  missing FROM-clause entry for table "notes"
       LINE 1: ...."deleted" = 'f' AND "volunteers"."id" = 7798 AND "notes"."s...
                                                                    ^
       : SELECT  "volunteers".* FROM "volunteers"  WHERE "volunteers"."deleted" = 'f' AND "volunteers"."id" = 7798 AND "notes"."subject_type" = 'Volunteer' LIMIT 1
     # ./spec/models/note_spec.rb:10:in `block (3 levels) in <top (required)>'

Why?

The reason I want to do it this way is because I'm constructing a scope based on parsing a query string including joining to various models/etc; the code used to construct the scope is considerably more complex than that above - it uses collection.reflections, etc. My current solution works for this, but it offends me I can't call the relations directly from an instance of Note.

I could solve it by splitting it into two issues: using scopes directly

  scope :scoped_by_volunteer_id, lambda { |volunteer_id| where({subject_type: 'Volunteer', subject_id: volunteer_id}) }
  scope :scoped_by_participation_id, lambda { |participation_id| where({subject_type: 'Participation', subject_id: participation_id}) }

and then just using a getter for note.volunteer/note.participation that just returns note.subject if it has the right subject_type (nil otherwise) but I figured in Rails there must be a better way?

like image 837
Benjie Avatar asked Feb 18 '14 10:02

Benjie


People also ask

Is polymorphism a type of association?

Polymorphic association is a term used in discussions of Object-Relational Mapping with respect to the problem of representing in the relational database domain, a relationship from one class to multiple classes.

What are polymorphic associations in Rails?

In Ruby on Rails, a polymorphic association is an Active Record association that can connect a model to multiple other models. For example, we can use a single association to connect the Review model with the Event and Restaurant models, allowing us to connect a review with either an event or a restaurant.

How would you choose between Belongs_to and Has_one?

The difference between belongs_to and has_one is a semantic one. The model that declares belongs_to includes a column containing the foreign key of the other. The model that declares has_one has its foreign key referenced.

Does Rails have one relationship?

Ruby on Rails ActiveRecord Associations has_oneA has_one association sets up a one-to-one connection with another model, but with different semantics. This association indicates that each instance of a model contains or possesses one instance of another model.


3 Answers

I had bump into the similar problem. and I finally ironed out the best and most robust solution by using a self reference association like below.

class Note < ActiveRecord::Base   # The true polymorphic association   belongs_to :subject, polymorphic: true    # The trick to solve this problem   has_one :self_ref, :class_name => self, :foreign_key => :id    has_one :volunteer, :through => :self_ref, :source => :subject, :source_type => Volunteer   has_one :participation, :through => :self_ref, :source => :subject, :source_type => Participation end 

Clean & simple, only tested on Rails 4.1, but I guess it should work for previous versions.

like image 142
StackNG Avatar answered Oct 10 '22 04:10

StackNG


I have found a hackish way of getting around this issue. I have a similar use case in a project of mine, and I found this to work. In your Note model you can add associations like this:

class Note
  belongs_to :volunteer, 
    ->(note) {where('1 = ?', (note.subject_type == 'Volunteer')},
    :foreign_key => 'subject_id'
end

You will need to add one of these for each model that you wish to attach notes to. To make this process DRYer I would recommend creating a module like so:

 module Notable
   def self.included(other)
     Note.belongs_to(other.to_s.underscore.to_sym, 
       ->(note) {where('1 = ?', note.subject_type == other.to_s)},
       {:foreign_key => :subject_id})
   end
 end

Then include this in your Volunteer and Participation models.

[EDIT]

A slightly better lambda would be:

 ->(note) {(note.subject_type == "Volunteer") ? where('1 = 1') : none}

For some reason replacing the 'where' with 'all' does not seem to work. Also note that 'none' is only available in Rails 4.

[MOAR EDIT]

I'm not running rails 3.2 atm so I can't test, but I think you can achieve a similar result by using a Proc for conditions, something like:

belongs_to :volunteer, :foreign_key => :subject_id, 
  :conditions => Proc.new {['1 = ?', (subject_type == 'Volunteer')]}

Might be worth a shot

like image 38
Slicedpan Avatar answered Oct 10 '22 05:10

Slicedpan


I was stuck on this sort of reverse association and in Rails 4.2.1 I finally discovered this. Hopefully this helps someone if they're using a newer version of Rails. Your question was the closest to anything I found in regard to the issue I was having.

belongs_to :volunteer, foreign_key: :subject_id, foreign_type: 'Volunteer'
like image 37
skilleo Avatar answered Oct 10 '22 05:10

skilleo