20 Stimmen

Wie funktionieren Rails-Vereinigungsmethoden?

Wie funktionieren Rails Association-Methoden? Betrachten wir dieses Beispiel

class User < ActiveRecord::Base
   has_many :articles
end

class Article < ActiveRecord::Base
   belongs_to :user
end

Jetzt kann ich etwas wie

@user = User.find(:first)
@user.articles

Dies ruft Artikel ab, die diesem Benutzer gehören. Bisher so gut.

Jetzt kann ich weitergehen und auf diesen Artikeln mit bestimmten Bedingungen suchen.

@user.articles.find(:all, :conditions => {:sector_id => 3})

Oder einfach eine Verknüpfungsmethode deklarieren wie

class User < ActiveRecord::Base
   has_many :articles do
     def of_sector(sector_id)
       find(:all, :conditions => {:sector_id => sector_id})
     end
   end
end

Und machen

@user.articles.of_sector(3)

Meine Frage ist nun, wie funktioniert dieses find auf dem Array von ActiveRecord-Objekten, die mithilfe der Verknüpfungsmethode abgerufen wurden? Denn wenn wir unsere eigene User-Instanzmethode namens articles implementieren und unsere eigene Implementierung schreiben, die uns genau die gleichen Ergebnisse wie die der Verknüpfungsmethode liefert, wird das Finden auf dem Abruf-Array von ActiveRecord-Objekten nicht funktionieren.

Meine Vermutung ist, dass die Verknüpfungsmethoden bestimmte Eigenschaften an das Array der abgerufenen Objekte anhängen, die eine weitere Abfrage mithilfe von find und anderen ActiveRecord-Methoden ermöglichen. Was ist die Reihenfolge der Codeausführung in diesem Fall? Wie könnte ich das validieren?

22voto

pjb3 Punkte 4961

Wie es tatsächlich funktioniert, ist, dass das Verbandsobjekt ein "Proxy-Objekt" ist. Die spezifische Klasse ist AssociationProxy. Wenn Sie sich Zeile 52 dieser Datei ansehen, sehen Sie:

instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }

Durch das Durchführen dieser Aktion existieren Methoden wie class nicht mehr auf diesem Objekt. Wenn Sie also class auf dieses Objekt aufrufen, erhalten Sie eine Methode fehlt. Es gibt also ein method_missing implementiert für das Proxy-Objekt, das den Methodenaufruf an das "Ziel" weiterleitet:

def method_missing(method, *args)
  if load_target
    unless @target.respond_to?(method)
      message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
      raise NoMethodError, message
    end

    if block_given?
      @target.send(method, *args)  { |*block_args| yield(*block_args) }
    else
      @target.send(method, *args)
    end
  end
end

Das Ziel ist ein Array, also wenn Sie class auf dieses Objekt aufrufen, sagt es, dass es ein Array ist, aber das liegt nur daran, dass das Ziel ein Array ist, die tatsächliche Klasse jedoch eine AssociationProxy ist, aber das sehen Sie nicht mehr.

Alle Methoden, die Sie hinzufügen, wie z.B. of_sector, werden dem Verbandsproxy hinzugefügt, sodass sie direkt aufgerufen werden. Methoden wie [] und class sind nicht auf dem Verbandsproxy definiert, sodass sie an das Ziel gesendet werden, das ein Array ist.

Um Ihnen zu zeigen, wie dies passiert, fügen Sie dies zur Zeile 217 dieser Datei in Ihrer lokalen Kopie von association_proxy.rb hinzu:

Rails.logger.info "AssociationProxy leitet den Aufruf der Methode `#{method.to_s}' an \"#{@target}\":#{@target.class.to_s} weiter"

Wenn Sie nicht wissen, wo sich diese Datei befindet, wird der Befehl gem which 'active_record/associations/association_proxy' es Ihnen mitteilen. Nun, wenn Sie class auf einen AssociationProxy aufrufen, sehen Sie eine Protokollmeldung, die Ihnen mitteilt, dass sie dies an das Ziel sendet, was deutlicher machen sollte, was passiert. Dies gilt alles für Rails 2.3.2 und könnte sich in anderen Versionen ändern.

10voto

EmFi Punkte 23295

Wie bereits erwähnt, erstellen die aktiven Datensatzverbände eine ganze Menge an praktischen Methoden. Sicher, Sie könnten Ihre eigenen Methoden schreiben, um alles abzurufen. Aber das ist nicht der Rails-Weg.

Der Rails-Weg ist das Ergebnis von zwei Leitsätzen. DRY (Don't Repeat Yourself) und "Konvention vor Konfiguration". Im Grunde genommen können durch Benennung von Dingen auf eine sinnvolle Weise einige robuste Methoden, die vom Framework bereitgestellt werden, den ganzen gemeinsamen Code abstrahieren. Der Code, den Sie in Ihrer Frage platzieren, ist das perfekte Beispiel für etwas, das durch einen einzigen Methodenaufruf ersetzt werden kann.

Wo diese praktischen Methoden wirklich glänzen, sind die komplexeren Situationen. Die Art von Dingen, die Verbindungsmodelle, Bedingungen, Validierungen usw. beinhalten.

Um Ihre Frage zu beantworten, wenn Sie etwas wie @user.articles.find(:all, :conditions => ["created_at > ?", Dienstag]) machen, bereitet Rails zwei SQL-Abfragen vor und verschmilzt sie dann zu einer. Wohingegen Ihre Version einfach die Liste der Objekte zurückgibt. Benannte Scopes tun dasselbe, überschreiten aber normalerweise keine Modellgrenzen.

Sie können dies überprüfen, indem Sie die SQL-Abfragen in der development.log überprüfen, während Sie diese Dinge in der Konsole aufrufen.

Lassen Sie uns also einen Moment über benannte Scopes sprechen, weil sie ein großartiges Beispiel dafür geben, wie Rails mit dem SQL umgeht, und ich denke, sie sind eine einfachere Möglichkeit zu zeigen, was hinter den Kulissen passiert, da sie keine Modellverbände benötigen, um sie zu zeigen.

Benannte Scopes können verwendet werden, um benutzerdefinierte Suchen eines Modells durchzuführen. Sie können verkettet oder sogar durch Verbände aufgerufen werden. Sie könnten problemlos benutzerdefinierte Suchfunktionen erstellen, die identische Listen zurückgeben, aber dann stoßen Sie auf die gleichen Probleme wie in der Frage erwähnt.

class Artikel < ActiveRecord::Base
  gehört zu :user
  hat viele :comments
  hat viele :commentators, :durch :comments, :class_name => "user"
  named_scope :edited_scope, :conditions => {:edited => true}
  named_scope :recent_scope, lambda do
    { :conditions => ["updated_at > ?", DateTime.now - 7.days]}

  def self.edited_method
    self.find(:all, :conditions => {:edited => true})
  end

  def self.recent_method
    self.find(:all, :conditions => ["updated_at > ?", DateTime.now - 7 days])
  end
end

Artikel.edited_scope
=>     # Array von Artikeln, die als bearbeitet gekennzeichnet wurden. 1 SQL-Abfrage.
Artikel.edited_method
=>     # Array von Artikeln, die als bearbeitet gekennzeichnet wurden. 1 SQL-Abfrage.
Array.edited_scope == Array.edited_method
=> true     # gibt identische Listen zurück.

Artikel.recent_scope
=>     # Array von Artikeln, die in den letzten 7 Tagen aktualisiert wurden. 1 SQL-Abfrage.
Artikel.recent_method
=>     # Array von Artikeln, die in den letzten 7 Tagen aktualisiert wurden. 1 SQL-Abfrage.
Array.recent_scope == Array.recent_method
=> true     # gibt identische Listen zurück.

Hier ändern sich die Dinge:

Artikel.edited_scope.recent_scope
=>     # Array von Artikeln, die sowohl bearbeitet wurden als auch in den letzten 7 Tagen aktualisiert wurden. 1 SQL-Abfrage.
Artikel.edited_method.recent_method 
=> # Fehler bei der Methode recent_scope auf Array

# Man kann nicht einmal mischen und kombinieren.
Artikel.edited_scope.recent_method
=>     # Fehler bei der Methode
Artikel.recent_method.edited_scope
=>     # Fehler bei der Methode

# Funktioniert sogar über Verbände hinweg.
@user.articles.edited.comments
=>     # Array von Kommentaren, die zu Artikeln gehören, die als bearbeitet markiert sind und zu @user gehören. 1 SQL-Abfrage. 

Prinzipiell erstellt jeder benannte Scope ein SQL-Fragment. Rails wird geschickt jedes andere SQL-Fragment in der Kette mit jedem anderen verschmelzen, um eine einzige Abfrage zu erzeugen, die genau das zurückgibt, was Sie wollen. Die von den Verbandsmethoden hinzugefügten Methoden funktionieren genauso. Daher integrieren sie sich nahtlos in benannte Scopes.

Der Grund, warum das Mischen und Kombinieren nicht funktioniert hat, ist derselbe wie der, dass die in der Frage definierte of_sector-Methode nicht funktioniert. edited_methods gibt ein Array zurück, während edited_scope (sowie find und alle anderen AR-Bequemlichkeitsmethoden, die als Teil einer Kette aufgerufen werden) ihr SQL-Fragment an die nächste Sache in der Kette weitergeben. Wenn es das letzte in der Kette ist, führt es die Abfrage aus. Ähnlich funktioniert dies auch nicht.

@edited = Artikel.edited_scope
@edited.recent_scope

Sie haben versucht, diesen Code zu verwenden. Hier ist der richtige Weg, dies zu tun:

class Benutzer < ActiveRecord::Base
   hat viele :articles do
     def of_sector(sector_id)
       find(:all, :conditions => {:sector_id => sector_id})
     end
   end
end

Um diese Funktionalität zu erreichen, möchten Sie dies tun:

class Artikel < ActiveRecord::Base
  gehört zu :user
  named_scope :of_sector, lambda do |*sectors|
    { :conditions => {:sector_id => sectors} }
  end
end

class Benutzer < ActiveRecord::Base
  hat viele :articles
end

Dann können Sie Dinge wie diese tun:

@user.articles.of_sector(4) 
=>    # Artikel, die zu @user gehören und im Sektor 4 sind
@user.articles.of_sector(5,6) 
=>    # Artikel, die zu @user gehören und entweder im Sektor 4 oder 5 sind
@user.articles.of_sector([1,2,3,]) 
=>    # Artikel, die zu @user gehören und entweder im Sektor 1,2 oder 3 sind

1voto

valachi221 Punkte 23

Wie zuvor erwähnt, wenn Sie Folgendes tun

@user.articles.class
=> Array

bekommen Sie tatsächlich ein Array. Das liegt daran, dass die Methode #class nicht definiert war, wie auch zuvor erwähnt.

Aber wie bekommen Sie die tatsächliche Klasse von @user.articles (die Proxy sein sollte)?

Object.instance_method(:class).bind(@user.articles).call
=> ActiveRecord::Associations::CollectionProxy

Und warum haben Sie zunächst ein Array bekommen? Weil die Methode #class an die CollectionProxy @target-Instanz delegiert wurde, was tatsächlich ein Array ist. Sie könnten hinter die Kulissen schauen, indem Sie etwas Ähnliches wie dies tun:

@user.articles.proxy_association

0voto

Staelen Punkte 7551

Wenn Sie eine Verknüpfung (has_one, has_many, etc.) erstellen, sagt das Modell, dass einige Methoden automatisch von ActiveRecord eingefügt werden sollen. Wenn Sie jedoch beschließen, eine Instanzmethode zu erstellen, die die Verknüpfung selbst zurückgibt, können Sie diese Methoden nicht verwenden.

Die Abfolge ist etwas wie folgt:

  1. richten Sie articles im User Modell ein, d.h. machen Sie ein has_many :articles
  2. ActiveRecord fügt automatisch praktische Methoden in das Modell ein (z.B. size, empty?, find, all, first, etc)
  3. richten Sie user im Article ein, d.h. machen Sie ein belongs_to :user
  4. ActiveRecord fügt automatisch praktische Methoden in das Modell ein (z.B. user=, etc)

Es ist also klar, dass wenn Sie eine Verknüpfung deklarieren, die Methoden automatisch von ActiveRecord hinzugefügt werden, was schön ist, da es eine enorme Menge Arbeit erledigt, die sonst manuell erledigt werden müsste =)

Sie können hier mehr darüber lesen: http://guides.rubyonrails.org/association_basics.html#detailed-association-reference

Ich hoffe, das hilft =)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X