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.:
<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
I believe I have figured out a decent way to handle this that also covers most use cases that one might need.
I will say, it is a hard problem to find an answer to, as it is hard to figure out how to ask the question, and also hard to weed out all the articles that are just standard Rails answers. I think this problem falls into the advanced ActiveRecord realm.
Essentially what we are trying to do is to add a relationship to the model and only use that association if certain prerequisites are met on the model where the association is made. For example, if I have class SomeModel, and it has belongs_to association called "some_association", we might want to apply some prerequisite conditions that must be true on the SomeModel record that influence whether :some_association returns a result or not. In the case of a polymorphic relationship, the prerequisite condition is that the polymorphic type column is a particular value, and if not that value, it should return nil.
The difficulty of solving this problem is compounded by the different ways. I know of three different modes of access: direct access on an instance (ex: SomeModel.first.some_association), :joins (Ex: SomeModel.joins(:some_association), and :includes (Ex: SomeModel.includes(:some_association)) (note: eager_load is just a variation on joins). Each of these cases needs to be handled in a specific way.
Today, as I've essentially been revisiting this problem, I came up with the following utility method that acts as a kind of wrapper method for belongs_to. I'm guessing a similar approach could be used for other association types.
# WARNING: the joiner table must not be aliased to something else in the query,
# A parent / child relationship on the same table probably would not work here
# TODO: figure out how to support a second argument scope being passed
def self.belongs_to_with_prerequisites(name, prerequisites: {}, **options)
base_class = self
belongs_to name, -> (object=nil) {
# For the following explanation, assume we have an ActiveRecord class "SomeModel" that has a belongs_to
# relationship on it called "some_association"
# Object will be one of the following:
# * nil - when this association is loaded via an :includes.
# For example, SomeModel.includes(:some_association)
# * an model instance - when this association is called directly on the referring model
# For example: SomeModel.first.some_association, object will equal SomeModel.first
# * A JoinDependency - when we are joining this association
# For example, SomeModel.joins(:some_assocation)
if !object.is_a?(base_class)
where(base_class.table_name => prerequisites)
elsif prerequisites.all? {|name, value| object.send(name) == value}
self
else
none
end
},
options
end
That method would need to be injected into ActiveRecord::Base.
Then we could use it like:
belongs_to_with_prerequisites :volunteer,
prerequisites: { subject_type: 'Volunteer' },
polymorphic: true,
foreign_type: :subject_type,
foreign_key: :subject_id
And it would allow us to do the following:
Note.first.volunteer
Note.joins(:volunteer)
Note.eager_load(:volunteer)
However, we'll get an error if we try to do this:
Note.includes(:volunteer)
If we run that last bit of code, it will tell us that the column subject_type does not exist on the volunteers table.
So we'd have to add a scope to the Notes class and use as follows:
class Note < ActiveRecord::Base
belongs_to_with_prerequisites :volunteer,
prerequisites: { subject_type: 'Volunteer' },
polymorphic: true,
foreign_type: :subject_type,
foreign_key: :subject_id
scope :with_volunteer, -> { includes(:volunteer).references(:volunteer) }
end
Note.with_volunteer
So at the end of the day, we don't have the extra join table that @stackNG's solution had, but that solution was definitely more eloquent and less hacky. Figured I'd post this anyway as it has been the result of a very thorough investigation and might help somebody else understand how this stuff works.
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'
You can do like so:
belongs_to :volunteer, -> {
where(notes: { subject_type: 'Volunteer' }).includes(:notes)
}, foreign_key: :subject_id
The includes
do the left join
so you have the notes
relation with all volunteer
and participation
. And you go through subject_id
to find your record.
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.