ruby-on-rails - multiple - rails polymorphic
Rieles: incluye con asociación polimórfica (6)
Leí este interesante artículo sobre el uso del polimorfismo para mejorar la actividad en Rails .
Terminamos con algo así como
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
end
Ahora, si dos de esos temas son, por ejemplo:
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
Con create_activities definido como
def create_activities
Activity.create(subject: self)
end
Y con invitados y etiquetas definidas como:
class Guest < ActiveRecord::Base
belongs_to :event
end
class Tag < ActiveRecord::Base
belongs_to :image
end
Si consultamos las últimas 20 actividades registradas, podemos hacer:
Activity.order(created_at: :desc).limit(20)
Tenemos un primer problema de consulta N + 1 que podemos resolver con:
Activity.includes(:subject).order(created_at: :desc).limit(20)
Pero luego, cuando llamamos invitados o etiquetas, tenemos otro problema de consulta N + 1.
¿Cuál es la forma correcta de resolver eso para poder usar la paginación?
¿Esto genera una consulta SQL válida o falla porque los events
no se pueden JOIN
con las tags
y las images
no se pueden JOIN
con los guests
?
class Activity < ActiveRecord::Base
self.per_page = 10
def self.feed
includes(subject: [:guests, :tags]).order(created_at: :desc)
end
end
# in the controller
Activity.feed.paginate(page: params[:page])
Esto usaría will_paginate .
Después de leer el artículo de Relación polimórfica de carga impaciente en Rails , terminé con la siguiente solución:
class ActivitiesController < ApplicationController
def index
activities = current_user.activities.page(:page)
@activities = Activities::PreloadForIndex.new(activities).run
end
end
class Activities::PreloadForIndex
def initialize(activities)
@activities = activities
end
def run
preload_for event(activities), subject: :guests
preload_for image(activities), subject: :tags
activities
end
private
def preload_for(activities, associations)
ActiveRecord::Associations::Preloader.new.preload(activities, associations)
end
def event(activities)
activities.select &:event?
end
def image(activities)
activities.select &:image?
end
end
Se espera que esto se arregle en rieles 5.0. Ya hay un problema y una solicitud de extracción.
https://github.com/rails/rails/pull/17479
https://github.com/rails/rails/issues/8005
He bifurcado los rieles y aplicado el parche a 4.2-stable y funciona para mí. Siéntase libre de usar mi tenedor, aunque no puedo garantizar que se sincronice con la corriente ascendente de forma regular.
Sugeriría agregar la asociación polimórfica a sus modelos de Event
e Guest
. documento polimórfico
class Event < ActiveRecord::Base
has_many :guests
has_many :subjects
after_create :create_activities
end
class Image < ActiveRecord::Base
has_many :tags
has_many :subjects
after_create :create_activities
end
y luego intenta hacer
Activity.includes(:subject => [:event, :guest]).order(created_at: :desc).limit(20)
Edición 2 : ahora estoy utilizando rails 4.2 y el polimorfismo de carga ansioso ahora es una característica :)
Editar : Esto pareció funcionar en la consola, pero por alguna razón, mi sugerencia de uso con los parciales a continuación todavía genera advertencias de N + 1 Stack Stack con la gema de la bala. Necesito investigar ...
Ok, encontré la solución ([edit] ¿o no?), Pero supone que conoces todos los tipos de temas.
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
Y ahora puedes hacer
Activity.includes(:part, event: :guests, image: :tags).order(created_at: :desc).limit(10)
Pero para una carga de trabajo ansiosa, debe usar, por ejemplo,
activity.event.guests.first
y no
activity.part.guests.first
Entonces, probablemente puedas definir un método para usar en lugar del tema
def eager_loaded_subject
public_send(subject.class.to_s.underscore)
end
Entonces ahora puedes tener una vista con
render partial: :subject, collection: activity
Un parcial con
# _activity.html.erb
render :partial => ''activities/'' + activity.subject_type.underscore, object: activity.eager_loaded_subject
Y dos parciales (ficticios)
# _event.html.erb
<p><%= event.guests.map(&:name).join('', '') %></p>
# _image.html.erb
<p><%= image.tags.first.map(&:name).join('', '') %></p>
image_activities = Activity.where(:subject_type => ''Image'').includes(:subject => :tags).order(created_at: :desc).limit(20)
event_activities = Activity.where(:subject_type => ''Event'').includes(:subject => :guests).order(created_at: :desc).limit(20)
activities = (image_activities + event_activities).sort_by(&:created_at).reverse.first(20)