Back to Blog

Rails 6.1 adds delegated_type to ActiveRecord

on 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.

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.

1# schema of Vehicle {id: Integer, type: String[car or motorcycle], name: String, mileage: Integer}
2class Vehicle < ApplicationRecord
3  # put common logic here
4end
5
6class Car < Vehicle
7  # put car specific code & validation
8end
9
10class Motorcycle < Vehicle
11  # put motorcycle-specific code & validation
12end

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.

1# schema {name: String, mileage: Integer}
2class Vehicle < ApplicationRecord
3  belongs_to :vehicleable, polymorphic: true
4end
5
6# schema {interior_color: String, adjustable_roof: Boolean}
7class Car < ApplicationRecord
8  has_one :vehicle, as: :vehicleable
9end
10
11# schema {bs4_engine: Boolean, tank_color: String}
12class Motorcycle < ApplicationRecord
13  has_one :vehicle, as: :vehicleable
14end

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.

1# creating new records
2>> bike = Motorcycle.create!( bs4_engine: false, tank_color: '#f2f2f2')
3#<Motorcycle id: 1, bs4_engine: true, tank_color: '#f2f2f2', created_at: "2021-01-17 ...">
4
5>> vehicle = Vehicle.create!(vehicleable: bike, name: 'TS-1987', mileage: 45)
6#<Vehicle id: 1, vehicleable_type: "Motorcycle", vehicleable_id: 1, name: "TS-1987", mileage: 45, created_at: ...">
7
8# query
9>> b1 = Motorcycle.find(1) #=> <Motorcycle id: 1, bs4_engine: true, tank_color: '#f2f2f2', created_at: "2021-01-17 ...">
10>> b1.vehicle.name  #=> TS-1987
11>> 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.

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

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.

1# creating new records
2>> vehicle1 = Vehicle.create!(vehicleable: Car.new(interior_color: '#fff', adjustable_roof: true), name: 'TS78Z', mileage: 89)
3#<Vehicle id: 3, vehicleable_type: "Car", vehicleable_id: 2, name: "TS78Z", mileage: 89, created_at: ...">
4
5>> vehicle2= Vehicle.create!(vehicleable: Motorcycle.new(bs4_engine: false, tank_color: '#ff00bb'), name: 'BL96', mileage: 45)
6#<Vehicle id: 4, vehicleable_type: "Motorcycle", vehicleable_id: 5, name: "BL96", mileage: 45, created_at: ...">
7
8# 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.

1# Get all Vehicles that are Cars
2>> Vehicle.cars
3#<ActiveRecord::Relation [#<Vehicle id: 5, vehicleable_type: "Car", vehicleable_id: 1, name: "TS78Z", ...">]>
4
5# Get all Vehicles that are Motorcycles
6>> Vehicle.motorcycles
7#<ActiveRecord::Relation [#<Vehicle id: 1, vehicleable_type: "Motorcycle", vehicleable_id: 1, name: "BL96", ...">]>
8
9
10>> vehicle = Vehicle.find(3)
11#<Vehicle id: 3, vehicleable_type: "Car", vehicleable_id: 2, name: "TS78Z", mileage: 89, created_at: ...">
12
13# check whether a Vehicle is a Car or Motorcycle
14>> vehicle.car?  #=> true
15>> vehicle.motorcylce? #=> false
16
17# get vehicleable
18>> vehicle.car # <Car id: 1, adjustable_roof: true, ...>
19>> 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.


You might also like

If you liked this blog post, check out similar ones from BigBinary