web-dev-qa-db-fra.com

Association de rails avec plusieurs clés étrangères

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?

Mettre à jour

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
65
JonathanSimmons

TL; DR

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. 

68
Arslan Ali

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
23
dre-hh

É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
17
Dwight

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. 

14
JonathanSimmons

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.

0
sunsoft