Related
I have model with polymorhphic reference to two other models. I've also included distinct references per this article eager load polymorphic so I can still do model-specific queries as part of my .where clause. My queries work so I can search for scores doing Score.where(athlete: {foo}), however, when I try to do a .create, I get an error because the distinct reference alias seems to be blinding Rails of my polymorphic reference during validation.
Given that athletes can compete individually and as part of a team:
class Athlete < ApplicationRecord
has_many :scores, as: :scoreable, dependent: :destroy
end
class Team < ApplicationRecord
has_many :scores, as: :scoreable, dependent: :destroy
end
class Score < ApplicationRecord
belongs_to :scoreable, polymorphic: true
belongs_to :athlete, -> { where(scores: {scoreable_type: 'Athlete'}) }, foreign_key: 'scoreable_id'
belongs_to :team, -> { where(scores: {scoreable_type: 'Team'}) }, foreign_key: 'scoreable_id'
def athlete
return unless scoreable_type == "Athlete"
super
end
def team
return unless scoreable_type == "Team"
super
end
end
When I try to do:
Athlete.first.scores.create(score: 5)
...or...
Score.create(score: 5, scoreable_id: Athlete.first.id, scoreable_type: "Athlete")
I get the error:
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: scores.scoreable_type
Thanks!
#blazpie, using your scoping suggestion worked for me.
"those scoped belongs_to can be easily substituted by scopes in Score: scope :for_teams, -> { where(scorable_type: 'Team') }
In Rails 5, given a relationship between two tables that involves joining them on multiple shared attributes, how can I form an association between the models corresponding to these tables?
SQL:
SELECT *
FROM trips
JOIN stop_times ON trips.guid = stop_times.trip_guid AND trips.schedule_id = stop_times.schedule_id
I tried the following configuration, which works in general...
class Trip < ApplicationRecord
has_many :stop_times, ->(trip){ where("stop_times.schedule_id = ?", trip.schedule_id) }, :inverse_of => :trip, :primary_key => :guid, :foreign_key => :trip_guid, :dependent => :destroy
end
class StopTime < ApplicationRecord
belongs_to :trip, :inverse_of => :stop_times, :primary_key => :guid, :foreign_key => :trip_guid
end
Trip.first.stop_times.first #> StopTime object, as expected
Trip.first.stop_times.first.trip #> Trip object, as expected
... but when I try to use it in more advanced queries, it triggers ArgumentError: The association scope 'stop_times' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported....
Trip.joins(:stop_times).first #=> the unexpected ArgumentError
StopTime.joins(:trip).first #> StopTime object, as expected
I understand what the error is referencing, but I'm unsure of how to fix it.
EDIT:
I was hoping a single association would be sufficient, but it has been noted two different associations can do the job:
class Trip < ApplicationRecord
has_many :stop_times,
->(trip){ where("stop_times.schedule_id = ?", trip.schedule_id) },
:primary_key => :guid,
:foreign_key => :trip_guid # use trip.stop_times instead of trip.joined_stop_times to avoid error about missing attribute due to missing join clause
has_many :joined_stop_times,
->{ where("stop_times.schedule_id = trips.schedule_id") },
:class_name => "StopTime",
:primary_key => :guid,
:foreign_key => :trip_guid # use joins(:joined_stop_times) instead of joins(:stop_times) to avoid error about instance-specific association
end
Trip.first.stop_times
Trip.eager_load(:joined_stop_times).to_a.first.joined_stop_times # executes a single query
If anyone reading this knows how to use a single association, please at-mention me.
I don't think it is the right solution, but it can help. You can add another similar instance independent association that will be used for preloading only. It will work with :joins and :eager_load but not with :preload.
Note that :includes might internally use either :eager_load or :preload. So, :includes will not always work with that association. You should explicitly use :eager_load instead.
class Trip < ApplicationRecord
has_many :preloaded_stop_times,
-> { where("stop_times.schedule_id = trips.schedule_id") },
class_name: "StopTime",
primary_key: :guid,
foreign_key: :trip_guid
end
# Usage
trips = Trip.joins(:preloaded_stop_times).where(...)
# ...
# with :eager_load
trips = Trip.eager_load(:preloaded_stop_times)
trips.each do |trip|
stop_times = trip.preloaded_stop_times
# ...
end
User and Organization have a many-to-many association through Relationship. The Relationship model includes several boolean variables about the relationship, such as moderator (true/false) and member (true/false). Also, I added a boolean called default that sets the default organization.
I require a validation that if (and only if) a user is a member of one or more organizations (member == true), one (and exactly 1) of these organizations has to have default == true.
So basically this means that if a user is member of multiple organizations, one of these organizations needs to be the default ánd if the user is a member of multiple organizations such a default organization has to exist.
How to write this validation? My current validation generates the following error upon seeding:
PG::SyntaxError: ERROR: syntax error at or near "default"
LINE 1: ...ERE (user_id = 1) AND (member = 't' and default = ...
^
: SELECT COUNT(*) FROM "relationships" WHERE (user_id = 1) AND (member = 't' and default = 't')
My implementation in the Relationship model:
validate :default
private
def default
#relationships = Relationship.where('user_id = ?', self.user_id)
#members = #relationships.where('member = ?', true)
#defaults = #members.where('default = ?', true)
# If more than 1 organization has been set as default for user
if #defaults.count > 1
#defaults.drop(0).each do |invalid|
invalid.update_columns(default: false)
end
end
# If user is member but has no default organization yet
if !#defaults.any? && #members.any?
#members.first.update_columns(default: true)
end
end
Update On the looks of it, I understand I shouldn't model it this way, and instead should use a has_one belongs_to relationship as #DavidAldridge suggests in his answer. But I don't understand how to model this relationship (see my comment below the answer). Any advice is very much appreciated.
The reason for this being difficult is that your data model is incorrect. The identity of a user's default organisation is an attribute of the user, not of the relationship, because there can be only one default per user. If you had a primary, secondary, tertiary organisation, then that would be an attribute of the relationship.
Instead of placing a "relationship is default for user" attribute on the Relationship, place a "default_relationship_id" attribute on the User so it ...
belongs_to :default_relationship
... and ...
has_one :default_organisation, :through => :default_relationship
This guarantees that:
Only one organisation can be the default for the user
There has to be a relationship between the user and its default organisation
You can also use :dependent => :nullify on the inverse association of :default_relationship, and easily test whether an individual relationship is the default based on whether:
self == user.default_relationship.
So something like:
class User << ActiveRecord::Base
has_many :relationships, :inverse_of => :user, :dependent => :destroy
has_many :organisations, :through => :relationships, :dependent => :destroy
belongs_to :default_relationship, :class_name => "Relationship", :foreign_key => :default_relationship_id, :inverse_of => :default_for_user
has_one :default_organisation, :through => :default_relationship, :source => :organisation
class Relationship << ActiveRecord::Base
belongs_to :user , :inverse_of => :relationships
belongs_to :organisation, :inverse_of => :relationships
has_one :default_for_user, :class_name => "User", :foreign_key => :default_relationship_id, :inverse_of => :default_relationship, :dependent => :nullify
class Organisation << ActiveRecord::Base
has_many :relationships, :inverse_of => :organisation, :dependent => :destroy
has_many :users , :through => :relationships
has_many :default_for_users, :through => :relationships, :source => :default_for_user
Hence you can do such simple matters as:
#user = User.find(34)
#user.default_organisation
Default organisation is also easily eager-loaded (not that it couldn't be otherwise, but no scope is required to do so).
#Brad Werth's correct that your validate method would work better as a callback.
I'd recommend something like this in your Relationship model:
before_save :set_default
private
def set_default
self.default = true unless self.user.relationships.where(member: true, default: true).any?
end
This should enforce that a user's relationship is set to default if none of the user's other relationships already are.
Change default to is_default (as pointed out by another user in comments, default is postgres keyword). Create separate migration for this. (Or you could quote it everywhere if you prefer to leave it be as it is.)
Then, there are two points.
First, why you need to check for single is_default organization every time? You just need to migrate your current data set, and then keep it consistent.
To migrate your current data set, create migration and write something like this there:
def self.up
invalid_defaults = Relationship.
where(member: true, is_default: true).
group(:user_id).
having("COUNT(*) > 1")
invalid_defaults.each do |relationship|
this_user_relationships = relationship.user.relationships.where(member: true, is_default: true)
this_user_relationships.where.not(id: this_user_relationships.first.id).update_all(is_default: false)
end
end
Just make sure to run this migration in off-peak hours, as it could take considerable amount of time to finish. Alternatevely, you can just run that code snippet from the server console itself (just test in in development environment beforehand, of course).
Then, use callback (as rightfully suggested by another commenter) to set the default organization when the record is updated
before_save :set_default
private
def set_default
relationships = Relationship.where(user_id: self.user_id)
members = relationships.where(member: true)
defaults = members.where(is_default: true)
# No need to migrate records in-place
# Change #any? to #exists?, to check existance via SQL, without actually fetching all the records
if !defaults.exists? && members.exists?
# Choosing the earliest record
members.first.update_columns(is_default: true)
end
end
To take the case into account where Organization is being edited, callback to organization should be added as well:
class Organization
before_save :unset_default
after_commit :set_default
private
# Just quque is_default for update...
def remember_and_unset_default
if self.is_default_changed? && self.is_default
#default_was_set = true
self.is_default = false
end
end
# And now update it in a multi-thread safe way: let the database handle multiple queries being sent at once,
# and let only one of them to actually complete, keeping base in always consistent state
def set_default
if #default_was_set
self.class.
# update this record...
where(id: self.id).
# but only if there is still ZERO default organizations for this user
# (thread-safety will be handled by database)
where(
"id IN (SELECT id FROM organizations WHERE member = ?, is_default = ?, user_id = ? GROUP BY user_id HAVING COUNT(*)=0)",
true, true, self.user_id
)
end
end
I want to be able to use two columns on one table to define a relationship. So using a task app as an example.
Attempt 1:
class User < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
So then Task.create(owner_id:1, assignee_id: 2)
This allows me to perform Task.first.owner which returns user one and Task.first.assignee which returns user two but User.first.task returns nothing. Which is because task doesn't belong to a user, they belong to owner and assignee. So,
Attempt 2:
class User < ActiveRecord::Base
has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end
class Task < ActiveRecord::Base
belongs_to :user
end
That just fails altogether as two foreign keys don't seem to be supported.
So what I want is to be able to say User.tasks and get both the users owned and assigned tasks.
Basically somehow build a relationship that would equal a query of Task.where(owner_id || assignee_id == 1)
Is that possible?
Update
I'm not looking to use finder_sql, but this issue's unaccepted answer looks to be close to what I want: Rails - Multiple Index Key Association
So this method would look like this,
Attempt 3:
class Task < ActiveRecord::Base
def self.by_person(person)
where("assignee_id => :person_id OR owner_id => :person_id", :person_id => person.id
end
end
class Person < ActiveRecord::Base
def tasks
Task.by_person(self)
end
end
Though I can get it to work in Rails 4, I keep getting the following error:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
TL;DR
class User < ActiveRecord::Base
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
end
Remove has_many :tasks in User class.
Using has_many :tasks doesn't make sense at all as we do not have any column named user_id in table tasks.
What I did to solve the issue in my case is:
class User < ActiveRecord::Base
has_many :owned_tasks, class_name: "Task", foreign_key: "owner_id"
has_many :assigned_tasks, class_name: "Task", foreign_key: "assignee_id"
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User"
belongs_to :assignee, class_name: "User"
# Mentioning `foreign_keys` is not necessary in this class, since
# we've already mentioned `belongs_to :owner`, and Rails will anticipate
# foreign_keys automatically. Thanks to #jeffdill2 for mentioning this thing
# in the comment.
end
This way, you can call User.first.assigned_tasks as well as User.first.owned_tasks.
Now, you can define a method called tasks that returns the combination of assigned_tasks and owned_tasks.
That could be a good solution as far the readability goes, but from performance point of view, it wouldn't be that much good as now, in order to get the tasks, two queries will be issued instead of once, and then, the result of those two queries need to be joined as well.
So in order to get the tasks that belong to a user, we would define a custom tasks method in User class in the following way:
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
This way, it will fetch all the results in one single query, and we wouldn't have to merge or combine any results.
Extending upon #dre-hh's answer above, which I found no longer works as expected in Rails 5. It appears Rails 5 now includes a default where clause to the effect of WHERE tasks.user_id = ?, which fails as there is no user_id column in this scenario.
I've found it is still possible to get it working with a has_many association, you just need to unscope this additional where clause added by Rails.
class User < ApplicationRecord
has_many :tasks, ->(user) {
unscope(:where).where(owner: user).or(where(assignee: user)
}
end
Rails 5:
you need to unscope the default where clause
see #Dwight answer if you still want a has_many associaiton.
Though User.joins(:tasks) gives me
ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
As it is no longer possible you can use #Arslan Ali solution as well.
Rails 4:
class User < ActiveRecord::Base
has_many :tasks, ->(user){ where("tasks.owner_id = :user_id OR tasks.assignee_id = :user_id", user_id: user.id) }
end
Update1:
Regarding #JonathanSimmons comment
Having to pass the user object into the scope on the User model seems like a backwards approach
You don't have to pass the user model to this scope.
The current user instance is passed automatically to this lambda.
Call it like this:
user = User.find(9001)
user.tasks
Update2:
if possible could you expand this answer to explain what's happening? I'd like to understand it better so I can implement something similar. thanks
Calling has_many :tasks on ActiveRecord class will store a lambda function in some class variable and is just a fancy way to generate a tasks method on its object, which will call this lambda. The generated method would look similar to following pseudocode:
class User
def tasks
#define join query
query = self.class.joins('tasks ON ...')
#execute tasks_lambda on the query instance and pass self to the lambda
query.instance_exec(self, self.class.tasks_lambda)
end
end
I worked out a solution for this. I'm open to any pointers on how I can make this better.
class User < ActiveRecord::Base
def tasks
Task.by_person(self.id)
end
end
class Task < ActiveRecord::Base
scope :completed, -> { where(completed: true) }
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
def self.by_person(user_id)
where("owner_id = :person_id OR assignee_id = :person_id", person_id: user_id)
end
end
This basically overrides the has_many association but still returns the ActiveRecord::Relation object I was looking for.
So now I can do something like this:
User.first.tasks.completed and the result is all completed task owned or assigned to the first user.
Since Rails 5 you can also do that which is the ActiveRecord safer way:
def tasks
Task.where(owner: self).or(Task.where(assignee: self))
end
My answer to Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations is just for you!
As for your code,here are my modifications
class User < ActiveRecord::Base
has_many :tasks, ->(user) { unscope(where: :user_id).where("owner_id = ? OR assignee_id = ?", user.id, user.id) }, class_name: 'Task'
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
Warning:
If you are using RailsAdmin and need to create new record or edit existing record,please don't do what I've suggested.Because this hack will cause problem when you do something like this:
current_user.tasks.build(params)
The reason is that rails will try to use current_user.id to fill task.user_id,only to find that there is nothing like user_id.
So,consider my hack method as an way outside the box,but don't do that.
Better way is using polymorphic association:
task.rb
class Task < ActiveRecord::Base
belongs_to :taskable, polymorphic: true
end
assigned_task.rb
class AssignedTask < Task
end
owned_task.rb
class OwnedTask < Task
end
user.rb
class User < ActiveRecord::Base
has_many :assigned_tasks, as: :taskable, dependent: :destroy
has_many :owned_tasks, as: :taskable, dependent: :destroy
end
In result, we can use it so:
new_user = User.create(...)
AssignedTask.create(taskable: new_user, ...)
OwnedTask.create(taskable: new_user, ...)
pp user.assigned_tasks
pp user.owned_tasks
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?
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.
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 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 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.
Here's a different option using a parameterized scope. Based on the foreign type, the scope will be set to either all or none so that the relation returns the related model if it's of the right type and nil otherwise:
class Note < ActiveRecord::Base
belongs_to :subject, polymorphic: true
belongs_to :volunteer,
-> (note) { note.subject_type == "Volunteer" ? all : none },
foreign_key: :subject_id
belongs_to :participation,
-> (note) { note.subject_type == "Participation" ? all : none },
foreign_key: :subject_id
end