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