We write about Ruby on Rails, React.js, React Native, remote work, open source, engineering and design.
Rails 6.1 adds delegated_type
to ActiveRecord which makes
it easier for models to share responsibilities.
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.
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
.
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.
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.