it-swarm.com.de

Schienenverbindung mit mehreren Fremdschlüsseln

Ich möchte in der Lage sein, zwei Spalten in einer Tabelle zu verwenden, um eine Beziehung zu definieren. Verwenden Sie also eine Task-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)

Dadurch kann ich Task.first.owner ausführen, der Benutzer eins und Task.first.assignee zurückgibt, der Benutzer zwei zurückgibt, aber User.first.task nichts zurückgibt. Die Aufgabe gehört nicht zu einem Benutzer, sie gehört zu Besitzer und Verantwortlicher. So,

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 scheitert einfach zusammen, da zwei Fremdschlüssel nicht unterstützt werden. 

Was ich also möchte, ist User.tasks sagen zu können und sowohl den Benutzern eigene als auch zugewiesene Aufgaben zu erhalten. 

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

Ist das möglich?

Aktualisieren

Ich schaue nicht nach Finder_sql, aber die inakzeptable Antwort dieser Ausgabe scheint dem zu entsprechen, was ich will: Rails - Multiple Index Key Association

So würde diese Methode 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 in Rails 4 zum Laufen bringen kann, erhalte ich die folgende Fehlermeldung:

ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
65
JonathanSimmons

TL; DR

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

Entfernen Sie has_many :tasks in der Klasse User.


Die Verwendung von has_many :tasks macht überhaupt keinen Sinn, da wir in der Tabelle tasks keine Spalte user_id haben. 

Was ich in meinem Fall getan habe, um das Problem 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 User.first.assigned_tasks sowie User.first.owned_tasks aufrufen.

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

Das könnte eine gute Lösung sein, soweit die Lesbarkeit reicht, aber aus Sicht der Performance wäre es nicht mehr so ​​gut wie jetzt. Um die tasks zu erhalten, werden zwei Abfragen statt einmal und dann die Das Ergebnis dieser beiden Abfragen muss ebenfalls verknüpft werden. 

Um die Aufgaben eines Benutzers zu ermitteln, definieren wir eine benutzerdefinierte tasks-Methode in der User-Klasse auf folgende Weise:

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üssen keine Ergebnisse zusammenführen oder kombinieren. 

68
Arslan Ali

Das Erweitern der obigen Antwort von @ dre-hh, die nicht mehr wie erwartet in Rails 5 funktioniert, scheint Rails 5 enthält jetzt eine Standardklausel where to der Effekt von WHERE tasks.user_id = ?, der fehlschlägt, da in diesem Szenario keine Spalte user_id vorhanden ist.

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

class User < ApplicationRecord
  has_many :tasks, ->(user) { unscope(:where).where("owner_id = :id OR assignee_id = :id", id: user.id) }
end
27
Dwight

Schienen 5: 

sie müssen die Standard-where-Klausel siehe @Dwight antworten, wenn Sie noch eine has_many-Verbindung wünschen.

Obwohl User.joins(:tasks) mir gibt 

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: Zu @JonathanSimmons Kommentar 

Das Benutzerobjekt in den Gültigkeitsbereich des Benutzermodells übergeben zu müssen, scheint ein Rückschritt zu sein

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

user = User.find(9001)
user.tasks

Update2: 

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

Wenn Sie has_many :tasks für die ActiveRecord-Klasse aufrufen, wird eine Lambda-Funktion in einer Klassenvariablen gespeichert. Dies ist nur eine einfache Möglichkeit, eine tasks-Methode für ihr Objekt zu generieren, die dieses Lambda aufrufen wird. 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
23
dre-hh

Ich habe dafür eine Lösung gefunden. Ich bin offen für alle Hinweise, wie ich dies 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 Wesentlichen die has_many-Verknüpfung, gibt jedoch das ActiveRecord::Relation-Objekt zurück, das ich gesucht hatte. 

Also kann ich jetzt so etwas machen: 

User.first.tasks.completed und das Ergebnis ist alle abgeschlossene Aufgabe, die dem ersten Benutzer gehört oder diesem zugeordnet ist. 

14
JonathanSimmons

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 angeht, 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, machen Sie bitte nicht das, was ich Ihnen vorgeschlagen habe.

current_user.tasks.build(params)

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

Betrachten Sie meine Hack-Methode als einen Weg außerhalb der Box, aber tun Sie das nicht.

0
sunsoft