In a system I am helping to develop a person can be linked to a myriad of things, including themselves, so we use a relation table PersonRelation
defined as follows
class PersonRelation
belongs_to :person
belongs_to :personifiable, :polymorphic => true
belongs_to :person_relation_type
end
So a person could be linked to different "personifiable" things, and sometimes the meaning of the relation can change (e.g. a person could be an owner or a renter --expressed by the relation type).
In our datamodel, a person is actually a "legal person", so it could also be an organisation, and an organisation can have contacts. Logically a contact belongs to one or more organisation by following the association in the reverse direction.
When using rails 3+ up until 4.0 we wrote the association as follows:
has_and_belongs_to_many :contacts,
:join_table => "person_relations",
:class_name => "Person",
:foreign_key => "person_id",
:association_foreign_key => "personifiable_id",
:readonly => false,
:conditions => ["personifiable_type = ? and people.archived_at is null and person_relations.archived_at is null and person_relation_type_id=?", "Person", PersonRelationType::CONTACT],
:insert_sql => proc {|record| "INSERT INTO person_relations(person_id, personifiable_id, personifiable_type, person_relation_type_id, created_at, updated_at) VALUES('#{self.id}', '#{record.id}', 'Person', 6, current_timestamp, current_timestamp)" }
has_and_belongs_to_many :organisations,
:join_table => "person_relations",
:class_name => "Person",
:association_foreign_key => "person_id",
:foreign_key => "personifiable_id",
:readonly => false,
:conditions => ["personifiable_type = ? and people.archived_at is null and person_relations.archived_at is null and person_relation_type_id=?", "Person", PersonRelationType::CONTACT],
:insert_sql => proc {|record| "INSERT INTO person_relations(person_id, personifiable_id, personifiable_type, person_relation_type_id, created_at, updated_at) VALUES('#{record.id}', '#{self.id}', 'Person', 6, current_timestamp, current_timestamp)" }
Pretty complicated, but it does the job :) Now we keep getting deprecation warning because :conditions, :insert_sql, :finder_sql
are all deprecated and essentially removed in rails 4.1+. I kept postponing because it seemed like really hard to translate. But one suggestion in the error-message is to use has_many :through
instead.
We already had the following statement in our Person
model:
has_many :person_relations
so for a simple linked model, we could just write
has_many :parcels, through: :person_relations, source: :personifiable, source_type: 'Parcel'
and likewise, for our contacts, we just have to add a condition, but on PersonRelation
, so we write:
has_many :contacts,
-> { where("person_relations.person_relation_type_id" => PersonRelationType::CONTACT)},
through: :person_relations,
class_name: "Person",
source: :personifiable,
source_type: 'Person'
I am not really happy with the explicit mention of person_relations
in the condition, this might impact chainability later on, but I am not sure how I could handle that differently. For now this does the job really cleanly.
Now the problem is how to follow the reverse association (from the contacts to the organisations), and actually this also proves pretty simple. If I "reverse" the person-relations first, than we can use that as the through
table:
has_many :reverse_person_relations, as: :personifiable, class_name: 'PersonRelation'
has_many :organisations,
-> { where("person_relations.person_relation_type_id" => PersonRelationType::CONTACT)},
through: :reverse_person_relations,
class_name: "Person",
source: :person
At first I felt that the deprecation of :insert_sql, :delete_sql, :finder_sql
would be an insurmountable hurdle, but actually it proved pretty simple to fix and in the end a lot easier and even more readable. Nice :+1: