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">

Success

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.

Credits

Learn More

Subscribe via RSS