Schienenassoziation mit mehreren Fremdschlüsseln

84

Ich möchte in der Lage sein, zwei Spalten in einer Tabelle zu verwenden, um eine Beziehung zu definieren. Verwenden Sie also eine Aufgaben-App als Beispiel.

Versuch 1:

class User < ActiveRecord::Base
  has_many :tasks
end

class Task < ActiveRecord::Base
  belongs_to :owner, class_name: "User", foreign_key: "owner_id"
  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end

Also dann Task.create(owner_id:1, assignee_id: 2)

Auf diese Weise kann ich ausführen, Task.first.ownerwelcher Benutzer eins und Task.first.assigneewelcher Benutzer zweiUser.first.task zurückgibt, aber nichts zurückgibt. Dies liegt daran, dass die Aufgabe keinem Benutzer gehört, sondern dem Eigentümer und dem Beauftragten . Damit,

Versuch 2:

class User < ActiveRecord::Base
  has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end

class Task < ActiveRecord::Base
  belongs_to :user
end

Das schlägt einfach ganz fehl, da zwei Fremdschlüssel nicht unterstützt zu werden scheinen.

Ich möchte also in der Lage sein User.tasks, sowohl die eigenen als auch die zugewiesenen Aufgaben der Benutzer zu sagen und zu erhalten.

Im Grunde irgendwie eine Beziehung aufbauen, die einer Abfrage von gleich wäre Task.where(owner_id || assignee_id == 1)

Ist das möglich?

Aktualisieren

Ich finder_sqlmöchte nicht verwenden , aber die nicht akzeptierte Antwort dieses Problems scheint nahe an dem zu liegen, was ich möchte: Rails - Multiple Index Key Association

Diese Methode würde also so aussehen:

Versuch 3:

class Task < ActiveRecord::Base
  def self.by_person(person)
    where("assignee_id => :person_id OR owner_id => :person_id", :person_id => person.id
  end 
end

class Person < ActiveRecord::Base

  def tasks
    Task.by_person(self)
  end 
end

Obwohl ich es zum Laufen bringen kann, erhalte Rails 4ich immer wieder den folgenden Fehler:

ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
JonathanSimmons
quelle
2
Ist dieses Juwel das, wonach Sie suchen? github.com/composite-primary-keys/composite_primary_keys
mus
Danke für die Info mus aber das ist nicht was ich suche. Ich möchte, dass eine Abfrage für eine oder eine Spalte ein bestimmter Wert ist. Kein zusammengesetzter Primärschlüssel.
JonathanSimmons
Ja, das Update macht es klar. Vergiss das Juwel. Wir dachten beide, Sie möchten nur einen zusammengesetzten Primärschlüssel verwenden. Dies sollte zumindest durch Definieren eines benutzerdefinierten Bereichs und einer Bereichsbeziehung möglich sein. Interessantes Szenario. Ich werde es mir später ansehen
dre-hh
FWIW mein Ziel hier ist es, eine bestimmte Benutzeraufgabe zu erhalten und das ActiveRecord :: Relation-Format beizubehalten, damit ich weiterhin Aufgabenbereiche für das Ergebnis für die Suche / Filterung verwenden kann.
JonathanSimmons

Antworten:

80

TL; DR

class User < ActiveRecord::Base
  def tasks
    Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
  end
end

has_many :tasksIn der UserKlasse entfernen .


Die Verwendung ist has_many :tasksüberhaupt nicht sinnvoll, da user_idin der Tabelle keine Spalte benannt ist tasks.

Was ich getan habe, um das Problem in meinem Fall zu lösen, ist:

class User < ActiveRecord::Base
  has_many :owned_tasks,    class_name: "Task", foreign_key: "owner_id"
  has_many :assigned_tasks, class_name: "Task", foreign_key: "assignee_id"
end

class Task < ActiveRecord::Base
  belongs_to :owner,    class_name: "User", foreign_key: "owner_id"
  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
  # Mentioning `foreign_keys` is not necessary in this class, since
  # we've already mentioned `belongs_to :owner`, and Rails will anticipate
  # foreign_keys automatically. Thanks to @jeffdill2 for mentioning this thing 
  # in the comment.
end

Auf diese Weise können Sie anrufen User.first.assigned_taskssowie User.first.owned_tasks.

Jetzt können Sie eine Methode mit dem Namen definieren tasks, die die Kombination von assigned_tasksund zurückgibt owned_tasks.

Das könnte eine gute Lösung sein, was die Lesbarkeit betrifft, aber aus Sicht der Leistung wäre es nicht so gut wie jetzt, um das zu erhalten tasks, werden zwei Abfragen statt einmal und dann das Ergebnis ausgegeben von diesen beiden Abfragen müssen auch verbunden werden.

Um die Aufgaben zu erhalten, die einem Benutzer gehören, definieren wir eine benutzerdefinierte tasksMethode in der UserKlasse folgendermaßen:

def tasks
  Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end

Auf diese Weise werden alle Ergebnisse in einer einzigen Abfrage abgerufen, und wir müssten keine Ergebnisse zusammenführen oder kombinieren.

Arslan Ali
quelle
Diese Lösung ist großartig! Dies setzt voraus, dass ein separater Zugriff für die Zuweisung, den Besitz und alle Aufgaben erforderlich ist. Etwas, das bei einem großen Projekt großartig wäre. Dies war in meinem Fall jedoch nicht erforderlich. Was has_many"nicht sinnvoll" betrifft Wenn Sie die akzeptierte Antwort lesen, werden Sie feststellen, dass wir überhaupt keine has_manyErklärung verwendet haben. Unter der Annahme, dass Ihre Anforderungen den Bedürfnissen jedes anderen Besuchers entsprechen, ist diese Antwort möglicherweise weniger wertend.
JonathanSimmons
Abgesehen davon glaube ich, dass dies das bessere langfristige Setup ist, das wir Menschen mit diesem Problem empfehlen sollten. Wenn Sie Ihre Antwort überarbeiten würden, um ein grundlegendes Beispiel für die anderen Modelle und die Benutzermethode zum Abrufen der kombinierten Aufgaben zu enthalten, werde ich sie als ausgewählte Antwort überarbeiten. Vielen Dank!
JonathanSimmons
Ja, ich stimme dir zu. Mit owned_tasksund assigned_tasksalle Aufgaben bekommen eine Performance Hinweis haben. Wie auch immer, ich habe meine Antwort aktualisiert und eine Methode in die UserKlasse aufgenommen, um alle zugehörigen tasksDaten zu erhalten. Dadurch werden die Ergebnisse in einer Abfrage abgerufen, und es muss kein Zusammenführen / Kombinieren durchgeführt werden.
Arslan Ali
2
Nur zu Ihrer Information, die im TaskModell angegebenen Fremdschlüssel sind eigentlich nicht erforderlich. Da Sie die haben belongs_toBeziehungen benannt :ownerund :assigneewird Rails standardmäßig davon aus, dass Fremdschlüssel benannt sind owner_idund assignee_idjeweils.
Jeffdill2
1
@ ArslanAli kein Problem. :-)
jeffdill2
47

Erweiterung auf die Antwort von @ dre-hh oben, die in Rails 5 nicht mehr wie erwartet funktioniert. Es scheint, dass Rails 5 jetzt eine Standard-where-Klausel enthält WHERE tasks.user_id = ?, die fehlschlägt, da user_idin diesem Szenario keine Spalte vorhanden ist .

Ich habe festgestellt, dass es immer noch möglich ist, es mit einer has_manyZuordnung zum Laufen zu bringen. Sie müssen nur diese zusätzliche where-Klausel deaktivieren, die von Rails hinzugefügt wurde.

class User < ApplicationRecord
  has_many :tasks, ->(user) {
    unscope(:where).where(owner: user).or(where(assignee: user)
  }
end
Dwight
quelle
Danke @Dwight, daran hätte ich nie gedacht!
Gabe Kopley
Dies kann für a nicht funktionieren belongs_to(wenn das übergeordnete Element keine ID hat, muss es also auf der mehrspaltigen PK basieren). Es heißt nur, dass die Beziehung Null ist (und wenn ich auf die Konsole schaue, kann ich nicht sehen, dass die Lambda-Abfrage jemals ausgeführt wird).
fgblomqvist
@fgblomqvist gehört_zu hat eine Option namens: primary_key, die die Methode angibt, die den Primärschlüssel des zugeordneten Objekts zurückgibt, das für die Zuordnung verwendet wird. Standardmäßig ist dies id. Ich denke, das könnte dir helfen.
Ahmed Kamal
@AhmedKamal Ich sehe nicht, wie das überhaupt nützlich ist?
fgblomqvist
24

Schienen 5:

Sie müssen die Standard-where-Klausel deaktivieren, siehe @ Dwight-Antwort, wenn Sie weiterhin eine has_many-Zuordnung wünschen.

Obwohl User.joins(:tasks)gibt mir

ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.

Da dies nicht mehr möglich ist, können Sie auch die @ Arslan Ali-Lösung verwenden.

Schienen 4:

class User < ActiveRecord::Base
  has_many :tasks, ->(user){ where("tasks.owner_id = :user_id OR tasks.assignee_id = :user_id", user_id: user.id) }
end

Update1: In Bezug auf @ JonathanSimmons Kommentar

Das Benutzerobjekt in den Bereich des Benutzermodells übergeben zu müssen, scheint ein Rückwärtsansatz zu sein

Sie müssen das Benutzermodell nicht an diesen Bereich übergeben. Die aktuelle Benutzerinstanz wird automatisch an dieses Lambda übergeben. Nennen Sie es so:

user = User.find(9001)
user.tasks

Update2:

Wenn möglich, können Sie diese Antwort erweitern, um zu erklären, was passiert? Ich würde es gerne besser verstehen, damit ich etwas Ähnliches implementieren kann. Vielen Dank

Das Aufrufen der has_many :tasksActiveRecord-Klasse speichert eine Lambda-Funktion in einer Klassenvariablen und ist nur eine ausgefallene Möglichkeit, eine tasksMethode für ihr Objekt zu generieren , die dieses Lambda aufruft. Die generierte Methode würde dem folgenden Pseudocode ähneln:

class User

  def tasks
   #define join query
   query = self.class.joins('tasks ON ...')
   #execute tasks_lambda on the query instance and pass self to the lambda
   query.instance_exec(self, self.class.tasks_lambda)
  end

end
dre-hh
quelle
1
So großartig, überall sah ich aus, dass die Leute immer wieder versuchten, dieses veraltete Juwel für zusammengesetzte Schlüssel
vorzuschlagen
2
Wenn möglich, können Sie diese Antwort erweitern, um zu erklären, was passiert? Ich würde es gerne besser verstehen, damit ich etwas Ähnliches implementieren kann. danke
Stephen Lead
1
Dies behält zwar die Assoziation bei, verursacht jedoch andere Probleme. aus den Dokumenten: Hinweis: Das Zusammenfügen, eifrige Laden und Vorladen dieser Zuordnungen ist nicht vollständig möglich. Diese Operationen werden vor der Instanzerstellung ausgeführt, und der Bereich wird mit dem Argument nil aufgerufen. Dies kann zu unerwartetem Verhalten führen und ist veraltet. api.rubyonrails.org/classes/ActiveRecord/Associations/…
s2t2
1
Das sieht vielversprechend aus, aber mit Rails 5 wird immer noch der Foreign_key mit einer whereAbfrage verwendet, anstatt nur das obige Lambda zu verwenden
Mohamed El Mahallawy
1
Ja, es scheint, als ob dies in Rails 5 nicht mehr funktioniert.
Dwight
13

Ich habe eine Lösung dafür erarbeitet. Ich bin offen für Hinweise, wie ich das verbessern kann.

class User < ActiveRecord::Base

  def tasks
    Task.by_person(self.id)
  end 
end

class Task < ActiveRecord::Base

  scope :completed, -> { where(completed: true) }   

  belongs_to :owner, class_name: "User", foreign_key: "owner_id"
  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"

  def self.by_person(user_id)
    where("owner_id = :person_id OR assignee_id = :person_id", person_id: user_id)
  end 
end

Dies überschreibt im Grunde die Zuordnung has_many, gibt aber immer noch das gesuchte ActiveRecord::RelationObjekt zurück.

Jetzt kann ich so etwas machen:

User.first.tasks.completed und das Ergebnis ist die gesamte abgeschlossene Aufgabe, die dem ersten Benutzer gehört oder ihm zugewiesen wurde.

JonathanSimmons
quelle
Verwenden Sie diese Methode immer noch, um Ihre Frage zu lösen? Ich bin im selben Boot und frage mich, ob Sie einen neuen Weg gelernt haben oder ob dies immer noch die beste Option ist.
Dan
Immer noch die beste Option, die ich gefunden habe.
JonathanSimmons
Das ist wahrscheinlich ein Überbleibsel alter Versuche. Die Antwort wurde bearbeitet, um sie zu entfernen.
JonathanSimmons
1
Würden diese und dre-hhs Antwort unten dasselbe bewirken?
dkniffin
1
> Das Übergeben des Benutzerobjekts an den Bereich des Benutzermodells scheint ein Rückwärtsansatz zu sein. Sie müssen nichts übergeben, dies ist ein Lambda und die aktuelle Benutzerinstanz wird automatisch an sie übergeben
dre-hh
2

Meine Antwort auf Assoziationen und (mehrere) Fremdschlüssel in Rails (3.2): Wie man sie im Modell beschreibt und Migrationen aufschreibt, ist nur für Sie!

Was Ihren Code betrifft, hier sind meine Änderungen

class User < ActiveRecord::Base
  has_many :tasks, ->(user) { unscope(where: :user_id).where("owner_id = ? OR assignee_id = ?", user.id, user.id) }, class_name: 'Task'
end

class Task < ActiveRecord::Base
  belongs_to :owner, class_name: "User", foreign_key: "owner_id"
  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end

Warnung: Wenn Sie RailsAdmin verwenden und einen neuen Datensatz erstellen oder einen vorhandenen Datensatz bearbeiten müssen, tun Sie bitte nicht das, was ich vorgeschlagen habe. Dieser Hack verursacht Probleme, wenn Sie Folgendes tun:

current_user.tasks.build(params)

Der Grund dafür ist, dass Rails versuchen, current_user.id zu verwenden, um task.user_id zu füllen, nur um festzustellen, dass es nichts Vergleichbares wie user_id gibt.

Betrachten Sie meine Hack-Methode als einen Weg über den Tellerrand hinaus, aber tun Sie das nicht.

Sunsoft
quelle
Ich bin mir nicht sicher, ob diese Antwort etwas enthält, was die genehmigte Antwort noch nicht getan hat. Darüber hinaus fühlt es sich wie eine Werbung für eine andere Frage an.
JonathanSimmons
@ JonathanSimmons Danke. Ich werde diese Antwort entfernen, wenn es schlecht ist, sich wie eine Anzeige zu verhalten.
Sunsoft
2

Seit Rails 5 können Sie auch das tun, was mit ActiveRecord sicherer ist:

def tasks
  Task.where(owner: self).or(Task.where(assignee: self))
end
user2309631
quelle