Identifying multiple instances of a polymorphic association in Rails
I recently had a problem which was solved by a polymorphic association and made things easy. Then came a twist: I needed model B
to have a reference to two identifiable instances of A
(polymorphic).
I’m having a hard time explaining what the problem is without an example so I hope that the rest of the post and examples will shed light on that.
The basics - a polymorphic association
I have a House
and a Car
. They both need to be protected by an AccessCode
of some sort. The user interface will be such that if you enter an access code, you’ll end up inside the House
or the Car
without really knowing what you’re getting into.
Polymorphic are such that a model can belong to more than one other model on a single association.
class AccessCode < ActiveRecord::Base
belongs_to :protected_belonging, polymorphic: true
before_create do
self.code = rand(100) # Imagine something complex and unique here :-)
end
end
class Car < ActiveRecord::Base
has_one :access_code, as: :protected_belonging
after_create do
self.create_access_code
end
end
class House < ActiveRecord::Base
has_one :access_code, as: :protected_belonging
after_create do
self.create_access_code
end
end
For polymorphic associations to work, the access_codes
table will need to hold the type as well as the id of the linked object.
class CreateExampleSchemas < ActiveRecord::Migration
def change
create_table :houses do |t|
t.string :address
end
create_table :cars do |t|
t.string :make
t.string :model
end
create_table :access_codes do |t|
t.integer :code
t.integer :protected_belonging_id
t.string :protected_belonging_type
end
end
end
Learn more about polymorphic associations from the Rails guides
With that set up, an AccessCode
can belong to multiple types of protected belongings. As I mentioned earlier, one goal I had set out from early on was that I should be able to find a protected belonging based on an entered access code.
> car = Car.create(make: "Subaru", model: "WRX")
#=> <Car id: 1, make: "Subaru", model: "WRX")
> car.access_code
#=> <AccessCode id: 1, protected_belonging_type: "Car", protected_belonging_id: 1, code: 5>
> AccessCode.find_by_code(5).protected_belonging
#=> <Car id: 1, make: "Subaru", model: "WRX">
> house = House.create(address: "123 Broadway St, NY")
#=> <House id: 1, address: "123 Broadway St, NY">
> house.access_code
#=> <AccessCode id: 2, protected_belonging_type: "House", protected_belonging_id: 1, code: 94>
> AccessCode.find_by_code(94).protected_belonging
#=> <House id: 1, address: "123 Broadway St, NY">
But what if my house has 2 doors to protect?
My house has two doors, a front and back door. I’d really like to protect each doors with their own unique access codes. I thought the solution was going to be as easy as linking House
to two AccessCode
objects.
class House < ActiveRecord::Base
has_one :front_door_access_code, as: :protected_belonging, class_name: "AccessCode"
has_one :back_door_access_code, as: :protected_belonging, class_name: "AccessCode"
after_create do
self.create_front_door_access_code
self.create_back_door_access_code
end
end
This will create 2 access codes successfully, but I don’t know which door I am allowed to open based on my access code.
> house = House.create(address: "123 Broadway St, NY")
#=> <House id: 1, address: "123 Broadway St, NY">
> house.front_door_access_code
#=> <AccessCode id: 1, protected_belonging_type: "House", protected_belonging_id: 1, access_code: 3>
> house.back_door_access_code
#=> <AccessCode id: 1, protected_belonging_type: "House", protected_belonging_id: 1, access_code: 3>
> AccessCode.all
#=> [
#=> <AccessCode id: 1, protected_belonging_type: "House", protected_belonging_id: 1, access_code: 3>,
#=> <AccessCode id: 2, protected_belonging_type: "House", protected_belonging_id: 1, access_code: 56>
#=> ]
This had me scratching my head for a bit and I couldn’t find any good solution online. I knew I’d have to manage a door_type
field in AccessCode
, but the thought of having to manage that explicitely when defining the association didn’t sit right with me. It also didn’t help that the door_type
concept doesn’t apply to all types of protected belongings.
Single table inheritance (STI) to the rescue
By making the front and back door access codes their own types, I don’t have to manage the type
field manually, Rails will handle it automagically.
To get STI going, the first thing needed is a type
field in the access_codes
table. This will allow Rails to save different types of objects in a single table, and return the right object types on fetch.
class AddTypeToAccessCodes < ActiveRecord::Migration
def change
add_column :access_codes, :type, :string, default: "AccessCode"
end
end
Read more about single table inheritance in the ActiveRecord documentation.
Once there’s a type
field in AccessCode
, subclasses can be defined. The House
class can also appropriately link to 2 access code types.
class FrontDoorAccessCode < AccessCode
end
class BackDoorAccessCode < AccessCode
end
class House < ActiveRecord::Base
has_one :front_door_access_code, as: :protected_belonging
has_one :back_door_access_code, as: :protected_belonging
after_create do
self.create_front_door_access_code
self.create_back_door_access_code
end
end
The results? Total security! It’s now possible to identify which door an access code opens.
> house = House.create(address: "123 Broadway St, NY")
#=> <House id: 1, address: "123 Broadway St, NY">
> car = Car.create(make: "Subaru", model: "WRX")
#=> <Car id: 1, make: "Subaru", model: "WRX")
> AccessCode.all
#=> [
#=> <FrontDoorAccessCode id: 1, type: "FrontDoorAccessCode", protected_belonging_type: "House", protected_belonging_id: 1, access_code: 3>
#=> <BackDoorAccessCode id: 2, type: "BackDoorAccessCode", protected_belonging_type: "House", protected_belonging_id: 1, access_code: 54>
#=> <AccessCode id: 3, type: "AccessCode", protected_belonging_type: "Car", protected_belonging_id: 1, code: 5>
#=> ]
> house.front_door_access_code
#=> <FrontDoorAccessCode id: 1, type: "FrontDoorAccessCode", protected_belonging_type: "House", protected_belonging_id: 1, access_code: 3>
> house.back_door_access_code
#=> <BackDoorAccessCode id: 2, type: "BackDoorAccessCode", protected_belonging_type: "House", protected_belonging_id: 1, access_code: 54>
> AccessCode.find_by_code(3).protected_belonging
#=> <House id: 1, address: "123 Broadway St, NY">
> AccessCode.find_by_code(54).protected_belonging
#=> <House id: 1, address: "123 Broadway St, NY">
> AccessCode.find_by_code(54).protected_belonging.front_door_access_code.code == 54
#=> true
Final thoughts
The same house will always be returned by the access code search. Given that I have an AccessCode
instance and I can get the door specific FrontDoorAccessCode
or BackDoorAccessCode
instances for a House
, I’ll be able to act accordingly based on that information.
Additionally, I’m not sure how I feel about having to create AccessCode
subclasses every time a protected belonging needs to be protected by more than one AccessCode. In my real word use case, I don’t think that this will happen often so I’m happy with this solution.