Rails 6.1 adds delegated_type to ActiveRecord

Akhil Gautam

Akhil Gautam

April 6, 2021

This blog is part of our  Rails 6.1 series.

Rails 6.1 adds delegated_type to ActiveRecord which makes it easier for models to share responsibilities.

Before Rails 6.1

Let's say we are building software to manage the inventory of an automobile company. It produces 2 types of vehicles, Car and Motorcycle. Both have name and mileage attributes.

Let's look into at least 2 different solutions to design this system. <br/>

Single Table Inheritance

In this approach, we combine all the attributes of various models and store them in a single table. Let's create a Vehicle model and its corresponding table to store the data of both Car and Motorcycle.

# schema of Vehicle {id: Integer, type: String[car or motorcycle], name: String, mileage: Integer}
class Vehicle < ApplicationRecord
  # put common logic here
end

class Car < Vehicle
  # put car specific code & validation
end

class Motorcycle < Vehicle
  # put motorcycle-specific code & validation
end

This approach fits precisely for this scenario but when the attributes of the various models differ, it becomes a pain point. Let's say at some point in time we add a bs4_engine boolean column to track whether a Motorcyle has a bs4_engine or not. In the case of Car, bs4_engine will contain nil. As time passes, a lot of vehicle-specific attributes get added and the database will be sparsely filled with a lot of nil.

Polymorphic Relations

With polymorphic associations, a model can belong to more than one other model, on a single association.

# schema {name: String, mileage: Integer}
class Vehicle < ApplicationRecord
  belongs_to :vehicleable, polymorphic: true
end

# schema {interior_color: String, adjustable_roof: Boolean}
class Car < ApplicationRecord
  has_one :vehicle, as: :vehicleable
end

# schema {bs4_engine: Boolean, tank_color: String}
class Motorcycle < ApplicationRecord
  has_one :vehicle, as: :vehicleable
end

Here, Vehicle is a class that contains common attributes, while Motorcycle and Car store any diverging attributes. This approach fixes the nil values, but to create a Vehicle record we now have to create a Car or Motorcycle first separately.

# creating new records
>> bike = Motorcycle.create!( bs4_engine: false, tank_color: '#f2f2f2')
#<Motorcycle id: 1, bs4_engine: true, tank_color: '#f2f2f2', created_at: "2021-01-17 ...">

>> vehicle = Vehicle.create!(vehicleable: bike, name: 'TS-1987', mileage: 45)
#<Vehicle id: 1, vehicleable_type: "Motorcycle", vehicleable_id: 1, name: "TS-1987", mileage: 45, created_at: ...">

# query
>> b1 = Motorcycle.find(1) #=> <Motorcycle id: 1, bs4_engine: true, tank_color: '#f2f2f2', created_at: "2021-01-17 ...">
>> b1.vehicle.name  #=> TS-1987
>> b1.vehicle.mileage  #=> 45

Now, let's say, we need to query Vehicles that are Motorcycles, or let's say we want to check whether a Vehicle is a Car or not. For all of these, we will have to write cumbersome logic and queries.

Rails 6.1 delegated_type

Rails 6.1 brings delegated_type which fixes the problem discussed above and adds a lot of helper methods. To use it, we just need to replace polymorphic relation with delegated_types.

class Vehicle < ApplicationRecord
  delegated_type :vehicleable, types: %w[ Motorcycle Car ]
end

That is the only change we need to make to leverage the delegated_type. With this change, we can create both the delegator and delegatee at the same time.

# creating new records
>> vehicle1 = Vehicle.create!(vehicleable: Car.new(interior_color: '#fff', adjustable_roof: true), name: 'TS78Z', mileage: 89)
#<Vehicle id: 3, vehicleable_type: "Car", vehicleable_id: 2, name: "TS78Z", mileage: 89, created_at: ...">

>> vehicle2= Vehicle.create!(vehicleable: Motorcycle.new(bs4_engine: false, tank_color: '#ff00bb'), name: 'BL96', mileage: 45)
#<Vehicle id: 4, vehicleable_type: "Motorcycle", vehicleable_id: 5, name: "BL96", mileage: 45, created_at: ...">

# Note: Just initializing the delegatee(Car.new/Motorcycle.new) is sufficient.

When it comes to query capabilities, it adds a lot of delegated type convenience methods.

# Get all Vehicles that are Cars
>> Vehicle.cars
#<ActiveRecord::Relation [#<Vehicle id: 5, vehicleable_type: "Car", vehicleable_id: 1, name: "TS78Z", ...">]>

# Get all Vehicles that are Motorcycles
>> Vehicle.motorcycles
#<ActiveRecord::Relation [#<Vehicle id: 1, vehicleable_type: "Motorcycle", vehicleable_id: 1, name: "BL96", ...">]>


>> vehicle = Vehicle.find(3)
#<Vehicle id: 3, vehicleable_type: "Car", vehicleable_id: 2, name: "TS78Z", mileage: 89, created_at: ...">

# check whether a Vehicle is a Car or Motorcycle
>> vehicle.car?  #=> true
>> vehicle.motorcylce? #=> false

# get vehicleable
>> vehicle.car # <Car id: 1, adjustable_roof: true, ...>
>> vehicle.motorcycle # nil

So, delegated_type can be thought of as sugar on top of polymorphic relations that adds convenience methods.

Check out the pull request to learn more.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.