Associazione Rails con più chiavi esterne

Voglio essere in grado di utilizzare due colonne su una tabella per definire una relazione. Quindi, utilizzando un’app di attività come esempio.

Tentativo 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 

Quindi, Task.create(owner_id:1, assignee_id: 2)

Ciò mi consente di eseguire Task.first.owner che restituisce l’ utente uno e Task.first.assignee che restituisce l’ utente due ma User.first.task non restituisce nulla. Il motivo è che l’attività non appartiene a un utente, ma appartiene al proprietario e all’assegnatario . Così,

Tentativo 2:

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

Questo fallisce del tutto poiché due chiavi esterne non sembrano essere supportate.

Quindi, quello che voglio è poter dire User.tasks e ottenere sia gli utenti di proprietà che i compiti assegnati.

Fondamentalmente in qualche modo build una relazione che sarebbe uguale a una query di Task.where(owner_id || assignee_id == 1)

È ansible?

Aggiornare

Non sto cercando di usare finder_sql , ma la risposta non accettata di questo problema sembra essere vicino a ciò che voglio: Rails – Multiple Index Key Association

Quindi questo metodo sarebbe simile a questo,

Tentativo 3:

 class Task  :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 

Anche se riesco a farlo funzionare su Rails 4 , continuo a ricevere il seguente errore:

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

TL; DR

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

Rimuovi has_many :tasks in User class.


Usando has_many :tasks non hanno alcun senso dato che non abbiamo nessuna colonna denominata user_id nelle tasks tabella.

Quello che ho fatto per risolvere il problema nel mio caso è:

 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 

In questo modo, puoi chiamare User.first.assigned_tasks e User.first.owned_tasks .

Ora puoi definire un metodo chiamato tasks che restituisce la combinazione di owned_tasks e owned_tasks .

Questa potrebbe essere una buona soluzione per quanto riguarda la leggibilità, ma dal punto di vista delle prestazioni, non sarebbe molto utile come ora, al fine di ottenere le tasks , verranno emesse due query invece di una volta, e quindi, risultato di quelle due domande devono essere uniti pure.

Quindi, per ottenere le attività che appartengono a un utente, definiremmo un metodo di tasks personalizzate nella class User nel modo seguente:

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

In questo modo, recupererà tutti i risultati in una singola query e non dovremo unire o combinare alcun risultato.

Rails 5:

è necessario distriggersre la clausola predefinita where vedere la risposta @Dwight se si desidera ancora un’associazione con has_many.

Sebbene User.joins(:tasks) mi dia

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

Poiché non è più ansible, puoi utilizzare anche la soluzione @Arslan Ali.

Rotaie 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: per quanto riguarda il commento di @JonathanSimmons

Dover passare l’object utente nello scope sul modello User sembra un approccio al contrario

Non è necessario passare il modello utente a questo ambito. L’istanza utente corrente viene passata automaticamente a questo lambda. Chiamalo così:

 user = User.find(9001) user.tasks 

Update2:

se ansible potresti espandere questa risposta per spiegare cosa sta succedendo? Mi piacerebbe capirlo meglio per poter implementare qualcosa di simile. Grazie

Chiamando has_many :tasks sulla class ActiveRecord memorizzeranno una funzione lambda in alcune variabili di class ed è solo un modo ingegnoso per generare un metodo di tasks sul suo object, che chiamerà questo lambda. Il metodo generato sarebbe simile al seguente pseudocodice:

 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 

Ho elaborato una soluzione per questo. Sono aperto a qualsiasi suggerimento su come posso renderlo migliore.

 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 

Questo in pratica sostituisce l’associazione has_many ma restituisce comunque l’object ActiveRecord::Relation che stavo cercando.

Quindi ora posso fare qualcosa del genere:

User.first.tasks.completed e il risultato è tutto l’attività completata di proprietà o assegnata al primo utente.

Prolungando la risposta di @ dre-hh qui sopra, che ho trovato non funziona più come previsto in Rails 5. Sembra che Rails 5 ora includa una clausola where dove l’effetto di WHERE tasks.user_id = ? , che fallisce in quanto non esiste una colonna user_id in questo scenario.

Ho scoperto che è ancora ansible farlo funzionare con un’associazione has_many , devi solo annullare l’esclusione di questa ulteriore clausola where aggiunta da Rails.

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

La mia risposta alle associazioni e (più) chiavi esterne in rotaia (3.2): come descriverle nel modello e scrivere le migrazioni è solo per te!

Per quanto riguarda il tuo codice, ecco le mie modifiche

 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 

Attenzione: se stai usando RailsAdmin e hai bisogno di creare un nuovo record o modificare un record esistente, ti preghiamo di non fare ciò che ho suggerito. Perché questo hack causerà problemi quando fai qualcosa del genere:

 current_user.tasks.build(params) 

Il motivo è che i binari proveranno a usare current_user.id per riempire task.user_id, solo per scoprire che non c'è nulla come user_id.

Quindi, considera il mio metodo di hacking come un modo fuori dagli schemi, ma non farlo.