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?
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
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.