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