I read this interesting article about Using Polymorphism to Make a Better Activity Feed in Rails.
We end up with something like
class Activity < ActiveRecord::Base belongs_to :subject, polymorphic: true end
Now, if two of those subjects are for example:
class Event < ActiveRecord::Base has_many :guests after_create :create_activities has_one :activity, as: :subject, dependent: :destroy end class Image < ActiveRecord::Base has_many :tags after_create :create_activities has_one :activity, as: :subject, dependent: :destroy end
With create_activities defined as
def create_activities Activity.create(subject: self) end
And with guests and tags defined as:
class Guest < ActiveRecord::Base belongs_to :event end class Tag < ActiveRecord::Base belongs_to :image end
If we query the last 20 activities logged, we can do:
Activity.order(created_at: :desc).limit(20)
We have a first N+1 query issue that we can solve with:
Activity.includes(:subject).order(created_at: :desc).limit(20)
But then, when we call guests or tags, we have another N+1 query problem.
What's the proper way to solve that in order to be able to use pagination ?
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.
Association in Rails defines the relationship between models. It is also the connection between two Active Record models. To figure out the relationship between models, we have to determine the types of relationship. Whether it; belongs_to, has_many, has_one, has_one:through, has_and_belongs_to_many.
Edit 2: I'm now using rails 4.2 and eager loading polymorphism is now a feature :)
Edit: This seemed to work in the console, but for some reason, my suggestion of use with the partials below still generates N+1 Query Stack warnings with the bullet gem. I need to investigate...
Ok, I found the solution ([edit] or did I ?), but it assumes that you know all subjects types.
class Activity < ActiveRecord::Base belongs_to :subject, polymorphic: true belongs_to :event, -> { includes(:activities).where(activities: { subject_type: 'Event' }) }, foreign_key: :subject_id belongs_to :image, -> { includes(:activities).where(activities: { subject_type: 'Image' }) }, foreign_key: :subject_id end
And now you can do
Activity.includes(:part, event: :guests, image: :tags).order(created_at: :desc).limit(10)
But for eager loading to work, you must use for example
activity.event.guests.first
and not
activity.part.guests.first
So you can probably define a method to use instead of subject
def eager_loaded_subject public_send(subject.class.to_s.underscore) end
So now you can have a view with
render partial: :subject, collection: activity
A partial with
# _activity.html.erb render :partial => 'activities/' + activity.subject_type.underscore, object: activity.eager_loaded_subject
And two (dummy) partials
# _event.html.erb <p><%= event.guests.map(&:name).join(', ') %></p> # _image.html.erb <p><%= image.tags.first.map(&:name).join(', ') %></p>
This will hopefully be fixed in rails 5.0. There is already an issue and a pull request for it.
https://github.com/rails/rails/pull/17479
https://github.com/rails/rails/issues/8005
I have forked rails and applied the patch to 4.2-stable and it works for me. Feel free to use my fork, even though I cannot guarantee to sync with upstream on a regular basis.
https://github.com/ttosch/rails/tree/4-2-stable
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With