web-dev-qa-db-fra.com

Rails: inclus avec association polymorphe

J'ai lu cet article intéressant sur tilisation du polymorphisme pour créer un meilleur flux d'activité dans les rails .

On se retrouve avec quelque chose comme

class Activity < ActiveRecord::Base
  belongs_to :subject, polymorphic: true
end

Maintenant, si deux de ces sujets sont par exemple:

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

Avec create_activities défini comme

def create_activities
  Activity.create(subject: self)
end

Et avec des invités et des balises définis comme:

class Guest < ActiveRecord::Base
  belongs_to :event
end

class Tag < ActiveRecord::Base
  belongs_to :image
end

Si nous interrogeons les 20 dernières activités enregistrées, nous pouvons faire:

Activity.order(created_at: :desc).limit(20)

Nous avons un premier problème de requête N + 1 que nous pouvons résoudre avec:

Activity.includes(:subject).order(created_at: :desc).limit(20)

Mais ensuite, lorsque nous appelons des invités ou des tags, nous avons un autre problème de requête N + 1.

Quelle est la bonne façon de résoudre ce problème afin de pouvoir utiliser la pagination?

33
Nycen

Nous espérons que cela sera corrigé dans Rails 5.0. Il y a déjà un problème et une demande d'extraction pour cela.

https://github.com/Rails/rails/pull/17479

https://github.com/Rails/rails/issues/8005

J'ai forké Rails et appliqué le patch à 4.2-stable et cela fonctionne pour moi. N'hésitez pas à utiliser mon fork, même si je ne peux pas garantir de synchroniser avec l'amont régulièrement.

https://github.com/ttosch/Rails/tree/4-2-stable

9
tosch

Edit 2: J'utilise maintenant Rails 4.2 et le polymorphisme de chargement est maintenant une fonctionnalité :)

Edit: Cela semblait fonctionner dans la console, mais pour une raison quelconque, ma suggestion d'utilisation avec les partiels ci-dessous génère toujours des avertissements N + 1 Query Stack avec la puce. Je dois enquêter ...

D'accord, j'ai trouvé la solution ([modifier] ou ai-je fait?), Mais cela suppose que vous connaissez tous les types de sujets.

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

Et maintenant vous pouvez faire

Activity.includes(:part, event: :guests, image: :tags).order(created_at: :desc).limit(10)

Mais pour que le chargement désireux fonctionne, vous devez utiliser par exemple

activity.event.guests.first

et pas

activity.part.guests.first

Vous pouvez donc probablement définir une méthode à utiliser à la place du sujet

def eager_loaded_subject
  public_send(subject.class.to_s.underscore)
end

Alors maintenant, vous pouvez avoir une vue avec

render partial: :subject, collection: activity

Un partiel avec

# _activity.html.erb
render :partial => 'activities/' + activity.subject_type.underscore, object: activity.eager_loaded_subject

Et deux (factices) partiels

# _event.html.erb
<p><%= event.guests.map(&:name).join(', ') %></p>

# _image.html.erb
<p><%= image.tags.first.map(&:name).join(', ') %></p>
16
Nycen

Après avoir lu Relations polymorphes à chargement intensif dans les rails article, je me suis retrouvé avec la solution suivante:

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
2
Hirurg103
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)
2
Hesham

Cela génère-t-il une requête SQL valide ou échoue-t-il parce que events ne peut pas être JOINed avec tags et images ne peut pas être JOINed avec 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])

Cela utiliserait will_paginate .

1
Dan Croak

Je suggère d'ajouter l'association polymorphe à vos modèles Event et Guest. doc polymorphe

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

puis essayez de faire

Activity.includes(:subject => [:event, :guest]).order(created_at: :desc).limit(20)
1
xlembouras