2017-03-13 4 views
9

Ich habe eine polymorphe Zuordnung (belongs_to :resource, polymorphic: true), wobei resource eine Vielzahl von verschiedenen Modellen sein kann. Zur Vereinfachung der Frage sei angenommen, dass es sich entweder um eine Order oder eine Customer handeln kann.Eager Last je nach Art der Verbindung in Ruby on Rails

Wenn es eine Order ist, möchte ich die Bestellung vorab laden und die Address vorladen. Wenn es ein Kunde ist, möchte ich die Customer vorladen und die Location laden.

Der Code dieser Assoziationen mit tut so etwas wie:

<%- @issues.each do |issue| -%> 
<%- case issue.resource -%> 
<%- when Customer -%> 
<%= issue.resource.name %> <%= issue.resource.location.name %> 
<%- when Order -%> 
<%= issue.resource.number %> <%= issue.resource.address.details %> 
<%- end -%> 

Derzeit meine Vorspannung verwendet:

@issues.preload(:resource) 

jedoch nach wie vor n-plus-eins-Ausgaben Ich sehe die bedingten Assoziationen zum Laden:

SELECT "addresses".* WHERE "addresses"."order_id" = ... 
SELECT "locations".* WHERE "locations"."customer_id" = ... 
... 

Was ist ein guter Weg, um das zu beheben? Ist es möglich, eine Assoziation manuell vorzuladen?

+1

Ich verwende polymorphe Assoziationen nicht länger aus Gründen wie dem Problem, dem Sie gegenüberstehen. (Sie machen auch Datenintegrität schwieriger). –

+0

Schienen ist ziemlich intelligent. Hast du '@ issues.preloads probiert (resouce: [: location,: address])' '? –

+0

@MarcRohloff es löst eine Ausnahme 'ActiveRecord :: AssociationNotFoundError' aus. Sieht nicht so aus, als würde das funktionieren (obwohl eine solche Syntax ideal wäre). – Stussa

Antwort

5

Sie können das mit Hilfe der ActiveRecord::Associations::Preloader Klasse tun. Hier ist der Code:

@issues = Issue.all # Or whatever query 
ActiveRecord::Associations::Preloader.new.preload(@issues.select { |i| i.resource_type == "Order" }, { resource: :address }) 
ActiveRecord::Associations::Preloader.new.preload(@issues.select { |i| i.resource_type == "Customer" }, { resource: :location }) 

Sie anderen Ansatz verwenden, wenn die Sammlung zu filtern. Zum Beispiel habe ich in meinem Projekt bin mit group_by

groups = sale_items.group_by(&:item_type) 
groups.each do |type, items| 
    conditions = case type 
    when "Product" then :item 
    when "Service" then { item: { service: [:group] } } 
end 

ActiveRecord::Associations::Preloader.new.preload(items, conditions) 

Sie können diesen Code in einiger Hilfsklasse leicht wickeln und es in verschiedenen Teilen Ihrer Anwendung verwenden.

0

Sie können Ihre polymorphe Assoziation in einzelne Assoziationen ausbrechen. Ich habe das verfolgt und bin sehr erfreut darüber, wie es meine Anwendungen vereinfacht hat.

class Issue 
    belongs_to :order 
    belongs_to :customer 

    # You should validate that one and only one of order and customer is present. 

    def resource 
    order || customer 
    end 
end 

Issue.preload(order: :address, customer: :location) 

Ich habe ein Juwel tatsächlich geschrieben, die dieses Muster einpackt, so dass die Syntax

class Issue 
    has_owner :order, :customer, as: :resource 
end 

wird und setzt entsprechend die Verbände und Validierungen auf. Leider ist diese Implementierung nicht offen oder öffentlich. Es ist jedoch nicht schwer, sich selbst zu tun.

+0

Danke für die Antwort. Unglücklicherweise suche ich nach einer Lösung, die mit polymorphen Assoziationen arbeitet und nicht die Möglichkeit hat, meine Assoziation als nicht-polymorph (im herkömmlichen Sinn) zu ändern. – Stussa

0

Ich habe eine brauchbare Lösung für mich selbst gefunden, als ich in diesem Problem steckte. Was ich befolgte, war, jeden Implementierungstyp zu durchlaufen und ihn zu einem Array zu verketten.

Um damit zu beginnen, werden wir zuerst notieren, welche Attribute für einen bestimmten Typ geladen werden.

ATTRIBS = { 
    'Order' => [:address], 
    'Customer' => [:location] 
}.freeze 

AVAILABLE_TYPES = %w(Order Customer).freeze 

Das obige listet die Zuordnungen auf, die für die verfügbaren Implementierungstypen eifrig geladen werden.

Jetzt in unserem Code, wir werden einfach durchlaufen AVAILABLE_TYPES und laden Sie dann die erforderlichen Verknüpfungen.

issues = [] 

AVAILABLE_TYPES.each do |type| 
    issues += @issues.where(resource_type: type).includes(resource: ATTRIBS[type]) 
end 

Dadurch haben wir eine Möglichkeit, die Zuordnungen basierend auf dem Typ vorzuladen.Wenn Sie einen anderen Typ haben, fügen Sie ihn einfach zu AVAILABLE_TYPES, und die Attribute zu ATTRIBS, und Sie werden fertig sein.

0

Sie benötigen Verbände in Modellen wie folgt zu definieren:

class Issue < ActiveRecord::Base 
    belongs_to :resource, polymorphic: true 

    belongs_to :order, -> { includes(:issues).where(issues: { resource_type: 'Order' }) }, foreign_key: :resource_id 
    belongs_to :customer, -> { includes(:issues).where(issues: { resource_type: 'Customer' }) }, foreign_key: :resource_id 
end 

class Order < ActiveRecord::Base 
    belongs_to :address 
    has_many :issues, as: :resource 
end 

class Customer < ActiveRecord::Base 
    belongs_to :location 
    has_many :issues, as: :resource 
end 

Jetzt können Sie erforderliche Vorspannung:

Issue.includes(order: :address, customer: :location).all 

In Ansichten sollten Sie explizite Beziehung Namen verwenden:

<%- @issues.each do |issue| -%> 
<%- case issue.resource -%> 
<%- when Customer -%> 
<%= issue.customer.name %> <%= issue.customer.location.name %> 
<%- when Order -%> 
<%= issue.order.number %> <%= issue.order.address.details %> 
<%- end -%> 

Das ist alles, nicht mehr n-plus-eins-Abfragen.

+0

Dieser Code funktioniert in Rails 5.0 und 4.2 –

0

Ich würde gerne eine meiner Fragen teilen, die ich für bedingtes eifriges Laden verwendet habe, aber nicht sicher, ob dies Ihnen helfen könnte, was ich nicht sicher bin, aber es ist einen Versuch wert.

i haben eine address Modell, das polymorphen zu user und property ist. So prüfe ich gerade die addressable_type manuell und rufen Sie dann die entsprechende Abfrage wie unten dargestellt: -

nach entweder user oder property bekommen, erhalte ich die address zu mit eager loading erforderlich Modelle

##@record can be user or property instance 
if @record.class.to_s == "Property" 
    Address.includes(:addressable=>[:dealers,:property_groups,:details]).where(:addressable_type=>"Property").joins(:property).where(properties:{:status=>"active"}) 
else if @record.class.to_s == "User" 
    Address.includes(:addressable=>[:pictures,:friends,:ratings,:interests]).where(:addressable_type=>"User").joins(:user).where(users:{is_guest:=>true}) 
end 

Die obige Abfrage ist ein kleines Snippet der tatsächlichen Abfrage, aber Sie können eine Idee darüber erhalten, wie man es für das eifrige Laden mit Joins verwendet, da es eine polymorphe Tabelle ist.

Ich hoffe, es hilft.

0

Wenn Sie das verknüpfte Objekt als Objekt instanziieren, z. Nenne es die Variable @object oder so etwas. Then the render should handle the determination of the correct view via the object's class. Dies ist eine Rails-Konvention, also die Magie von Schienen.

Ich hasse es, persönlich, weil es den aktuellen Bereich eines Fehlers zu debuggen, ohne so etwas wie byebug oder hebel so schwer ist, aber ich kann bestätigen, dass es funktioniert, wie wir es hier bei meinem Arbeitgeber nutzen ein ähnliches Problem zu lösen .

Statt schneller über das Vorladen, denke ich, das Geschwindigkeitsproblem wird besser durch diese Methode und Schienen-Caching gelöst.