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.owner
welcher Benutzer eins und Task.first.assignee
welcher 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_sql
mö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 4
ich immer wieder den folgenden Fehler:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
quelle
Antworten:
TL; DR
class User < ActiveRecord::Base def tasks Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id) end end
has_many :tasks
In derUser
Klasse entfernen .Die Verwendung ist
has_many :tasks
überhaupt nicht sinnvoll, dauser_id
in der Tabelle keine Spalte benannt isttasks
.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_tasks
sowieUser.first.owned_tasks
.Jetzt können Sie eine Methode mit dem Namen definieren
tasks
, die die Kombination vonassigned_tasks
und zurückgibtowned_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
tasks
Methode in derUser
Klasse 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.
quelle
has_many
"nicht sinnvoll" betrifft Wenn Sie die akzeptierte Antwort lesen, werden Sie feststellen, dass wir überhaupt keinehas_many
Erklärung verwendet haben. Unter der Annahme, dass Ihre Anforderungen den Bedürfnissen jedes anderen Besuchers entsprechen, ist diese Antwort möglicherweise weniger wertend.owned_tasks
undassigned_tasks
alle Aufgaben bekommen eine Performance Hinweis haben. Wie auch immer, ich habe meine Antwort aktualisiert und eine Methode in dieUser
Klasse aufgenommen, um alle zugehörigentasks
Daten zu erhalten. Dadurch werden die Ergebnisse in einer Abfrage abgerufen, und es muss kein Zusammenführen / Kombinieren durchgeführt werden.Task
Modell angegebenen Fremdschlüssel sind eigentlich nicht erforderlich. Da Sie die habenbelongs_to
Beziehungen benannt:owner
und:assignee
wird Rails standardmäßig davon aus, dass Fremdschlüssel benannt sindowner_id
undassignee_id
jeweils.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, dauser_id
in diesem Szenario keine Spalte vorhanden ist .Ich habe festgestellt, dass es immer noch möglich ist, es mit einer
has_many
Zuordnung 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
quelle
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).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 mirArgumentError: 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
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:
Das Aufrufen der
has_many :tasks
ActiveRecord-Klasse speichert eine Lambda-Funktion in einer Klassenvariablen und ist nur eine ausgefallene Möglichkeit, einetasks
Methode 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
quelle
where
Abfrage verwendet, anstatt nur das obige Lambda zu verwendenIch 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::Relation
Objekt 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.quelle
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:
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.
quelle
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
quelle