Beaucoup à plusieurs avec plusieurs autojointures ActiveRecord
-
28-10-2019 - |
Question
Je suis en train de mettre en œuvre des relations multiples entre les enregistrements du même modèle par Autojointures (basé sur @ réponse de Shtééf). Je les modèles suivants
create_table :relations, force: true do |t|
t.references :employee_a
t.string :rel_type
t.references :employee_b
end
class Relation < ActiveRecord::Base
belongs_to :employee_a, :class_name => 'Employee'
belongs_to :employee_b, :class_name => 'Employee'
end
class Employee < ActiveRecord::Base
has_many :relations, foreign_key: 'employee_a_id'
has_many :reverse_relations, class_name: 'Relation', foreign_key: 'employee_b_id'
has_many :subordinates, through: :relations, source: 'employee_b', conditions: {'relations.rel_type' => 'manager of'}
has_many :managers, through: :reverse_relations, source: 'employee_a', conditions: {'relations.rel_type' => 'manager of'}
end
Avec cette configuration, je peux accéder avec succès pour chaque enregistrement les listes des subordonnés et des gestionnaires. Cependant, j'ai du mal à créer des relations de la façon suivante
e = Employee.create
e.subordinates.create
e.subordinates #=> []
e.managers.create
e.managers #=> []
Le problème est que ce type de relations ne pas ensemble, donc je dois écrire
e = Employee.create
s = Employee.create
e.relations.create employee_b: s, rel_type: 'manager of'
e.subordinates #=> [#<Employee id:...>]
Ai-je fait quelque chose de mal?
La solution
Vous pouvez utiliser le rappel before_add
et before_remove
sur l'association has_many:
class Employee < ActiveRecord::Base
has_many :relations, foreign_key: 'employee_a_id'
has_many :reverse_relations, class_name: 'Relation', foreign_key: 'employee_b_id'
has_many :subordinates,
through: :relations,
source: 'employee_b',
conditions: {'relations.rel_type' => 'manager of'}
:before_add => Proc.new { |employe,subordinate| employe.relations.create(employe_b: subordinate, rel_type: 'manager of') },
:before_remove => Proc.new { |employe,subordinate| employe.relations.where(employe_b: subordinate, rel_type: 'manager of').first.destroy }
has_many :managers,
through: :reverse_relations,
source: 'employee_a',
conditions: {'relations.rel_type' => 'manager of'}
:before_add => Proc.new { |employe,manager| employe.reverse_relations.create(employe_a: manager, rel_type: 'manager of') },
:before_remove => Proc.new { |employe,manager| employe.reverse_relations.where(employe_b: subordinate, rel_type: 'manager of').first.destroy }
Il devrait fonctionner et vous rendre capable d'utiliser employe.managers.create
Vous pouvez utiliser instread build
de create
dans le rappel
vous pouvez également lire cette question sur cette solution
Autres conseils
Afin de créer un multiple many-to-many autojointure association, je recommande qu'il serait plus logique d'avoir plusieurs tables pour gérer la connexion. De cette façon, il est très clair du point de vue des données pour savoir exactement ce qui se passe, et il est également clair d'un point de vue logique. Donc, quelque chose le long de ces lignes:
create_table :manage_relation do |t|
t.references :employee_id
t.references :manager_id
end
create_table :subordinate_relation do |t|
t.references :employee_id
t.references :subordinate_id
end
class Employee < ActiveRecord::Base
has_many :subordinates,
:through => :subordinate_relation,
:class_name => "Employee",
:foreign_key => "subordinate_id"
has_many :managers,
:through => :manage_relation,
:class_name => "Employee",
:foreign_key => "manager_id"
belongs_to :employee,
:class_name => "Employee"
end
De cette façon, il ne devient pas plus alambiquée que nécessaire du point de vue de codage, et vous pouvez y accéder en utilisant les collections standard et il sera mis en place de façon appropriée vos connexions pour vous sans que vous ayez à les gérer. Ainsi, ces deux collections devrait fonctionner ..
employee.managers
employee.subordinates
Et vous pourriez ne pas avoir à gérer les autres variables. Sens? Il ajoute une table, mais améliore la clarté.
Je refaire vos modèles comme suit:
class ManagerRelation < ActiveRecord::Base
belongs_to :manager, :class_name => 'Employee'
belongs_to :subordinate, :class_name => 'Employee'
end
class Employee < ActiveRecord::Base
has_many :manager_relations, :class_name => "ManagerRelation",
:foreign_key => :subordinate_id
has_many :subordinate_relations, :class_name => "ManagerRelation",
:foreign_key => :manager_id
has_many :managers, :source => :manager,
:through => :manager_relations
has_many :subordinates, :source => :subordinate,
:through => :subordinate_relations
end
Maintenant, vous pouvez faire ce qui suit:
employee.managers
employee.subordinates
employee.managers << employee2
employee.subordinates << employee3
Remarque: Il est généralement un signe pour un de quitter l'entreprise quand ils sont faits pour rapport à deux gestionnaires: -)
Compte tenu de la relation présentée
create_table :relations, force: true do |t|
t.references :employee_a
t.string :rel_type
t.references :employee_b
end
class Employee < ActiveRecord::Base
has_many :subordinate_relations, :class_name => "Relation", :conditions => {:rel_type => 'manager of'}, :foreign_key => :employee_a
has_many :subordinates, :through => :subordinate_relations, :source => :subordinate, :foreign_key => :employee_b
has_many :manager_relations, :class_name => "Relation", :conditions => {:rel_type => 'manager of'}, :foreign_key => :employee_b
has_many :managers, :through => :manager_relations, :source => :manager, :foreign_key => :employee_a
end
class Relation < ActiveRecord::Base
belongs_to :manager, :class_name => "Employee", :foreign_key => :employee_a
belongs_to :subordinate, :class_name => "Employee", :foreign_key => :employee_b
end
e = Employee.create
e.subordinates.create #Employee ...
e.subordinates #[<Employee ...]
e2 = Employee.create
e2.managers.create #Employee
e2.managers #[<Employee ...]
Bien que la solution fonctionne - je suis un peu confus en liant les associations avec « rel_type ». Dans ce cas - je dirais que le rel_type est redondant et la relation devrait être mis en correspondance comme suit:
create_table :relations do |t|
t.reference :manager
t.reference :subordinate
end
Dans ce cas, la mise en correspondance d'association devrait être un peu plus simple.