Je veux pouvoir utiliser deux colonnes sur une table pour définir une relation. Donc, en utilisant une application de tâche comme exemple.
Tentative 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
Alors alors Task.create(owner_id:1, assignee_id: 2)
Cela me permet d'effectuer Task.first.owner
qui retourne utilisateur un et Task.first.assignee
qui renvoie utilisateur deux mais User.first.task
ne renvoie rien. C’est parce que la tâche n’appartient pas à un utilisateur, mais à propriétaire _ et destinataire. Alors,
Tentative 2:
class User < ActiveRecord::Base
has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end
class Task < ActiveRecord::Base
belongs_to :user
end
Cela échoue complètement car deux clés étrangères ne semblent pas être prises en charge.
Donc, ce que je veux, c'est pouvoir dire User.tasks
et obtenir à la fois les tâches possédées et attribuées aux utilisateurs.
Fondamentalement, construisez une relation qui équivaudrait à une requête de Task.where(owner_id || assignee_id == 1)
Est-ce possible?
Je ne cherche pas à utiliser Finder_sql
, mais la réponse non acceptée de ce problème semble être proche de ce que je veux: Rails - Association de clés à index multiples
Donc, cette méthode ressemblerait à ceci,
Tentative 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
Bien que je puisse le faire fonctionner dans Rails 4
, je continue à recevoir l'erreur suivante:
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
Supprimez has_many :tasks
dans la classe User
.
Utiliser has_many :tasks
n'a pas de sens du tout car nous n'avons aucune colonne nommée user_id
dans la table tasks
.
Ce que j'ai fait pour résoudre le problème dans mon cas est le suivant:
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
De cette façon, vous pouvez appeler User.first.assigned_tasks
ainsi que User.first.owned_tasks
.
Maintenant, vous pouvez définir une méthode appelée tasks
qui renvoie la combinaison de assigned_tasks
et owned_tasks
.
Cela pourrait être une bonne solution en ce qui concerne la lisibilité, mais du point de vue des performances, ce ne serait pas aussi bon que maintenant, afin d’obtenir le tasks
, deux requêtes seront émises au lieu d’une fois, puis le Le résultat de ces deux requêtes doit également être joint.
Ainsi, pour obtenir les tâches appartenant à un utilisateur, nous définirions une méthode tasks
personnalisée dans la classe User
de la manière suivante:
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
De cette façon, tous les résultats seront récupérés en une seule requête, sans qu'il soit nécessaire de fusionner ou de combiner les résultats.
Rails 5:
vous devez annuler la portée de la clause where par défaut voir @Dwight répondre si vous voulez toujours une association has_many.
Bien que User.joins(:tasks)
me donne
ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
Comme il n'est plus possible, vous pouvez également utiliser la solution @Arslan ALi.
Rails 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
Mise à jour 1: À propos du commentaire de @JonathanSimmons
Devoir passer l'objet utilisateur dans le périmètre du modèle utilisateur semble être une approche en arrière
Vous n'êtes pas obligé de passer le modèle utilisateur à cette étendue. L’instance d’utilisateur actuel est automatiquement transmise à ce lambda. Appelez ça comme ça:
user = User.find(9001)
user.tasks
Update2:
si possible, pourriez-vous développer cette réponse pour expliquer ce qui se passe? J'aimerais mieux comprendre afin que je puisse mettre en œuvre quelque chose de similaire. Merci
L'appel de has_many :tasks
sur la classe ActiveRecord stockera une fonction lambda dans une variable de classe et constitue simplement un moyen sophistiqué de générer une méthode tasks
sur son objet, qui appellera cette lambda. La méthode générée ressemblerait au pseudocode suivant:
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
Étendre la réponse de @ dre-hh ci-dessus, qui, selon moi, ne fonctionne plus comme prévu dans Rails 5. Il semble que Rails 5 inclut désormais une clause where par défaut à l'effet WHERE tasks.user_id = ?
, qui échoue car il n'y a pas de colonne user_id
dans ce scénario.
J'ai trouvé qu'il est toujours possible de le faire fonctionner avec une association has_many
, il vous suffit d'extraire la portée de cette clause where supplémentaire ajoutée par Rails.
class User < ApplicationRecord
has_many :tasks, ->(user) { unscope(:where).where("owner_id = :id OR assignee_id = :id", id: user.id) }
end
J'ai élaboré une solution pour cela. Je suis ouvert à toute indication sur la façon dont je peux améliorer cela.
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
Cela remplace fondamentalement l'association has_many mais renvoie toujours l'objet ActiveRecord::Relation
que je cherchais.
Alors maintenant, je peux faire quelque chose comme ça:
User.first.tasks.completed
et le résultat est que toute tâche terminée appartient ou est assignée au premier utilisateur.
Ma réponse à Associations et (multiples) clés étrangères dans Rails (3.2): comment les décrire dans le modèle et écrire les migrations est juste pour vous!
Quant à votre code, voici mes modifications
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
Attention: Si vous utilisez RailsAdmin et que vous devez créer un nouvel enregistrement ou modifier un enregistrement existant, veuillez ne pas suivre ce que je vous ai suggéré.
current_user.tasks.build(params)
La raison en est que Rails va essayer d'utiliser current_user.id pour remplir task.user_id, uniquement pour découvrir qu'il n'y a rien de tel que user_id.
Alors, considérez ma méthode de piratage comme un moyen de sortir de la boîte, mais ne le faites pas.