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_typeRails 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.
Follow @bigbinary on X. Check out our full blog archive.