📜 ⬆️ ⬇️

Polymorphic end-to-end associations in Ruby on Rails

The article deals with the method of creating polymorphism for many-to-many connections in Ruby on Rails.

Task


Assume that you need to develop a system for managing freight transport. At our disposal there are several types of this transport: trains, helicopters, trucks and barges. And it is known that each vehicle carries out transportation only to strictly defined settlements. For example, some trucks ride around the central part of Russia, some along the south, helicopters operate in Siberia and Kamchatka, trains are generally limited to railway tracks, and so on.
Each type of transport in the developed system will be represented by its class: Train , Copter , Truck , Ship, respectively.
Localities (cities, towns, research stations, here we are not interested in size, but in geographical coordinates), where transportation is carried out, are represented by the Location class.
There is a condition: any Location can be attached to each unit of transport. In turn, any number of vehicles of different types can be attached to each locality.



The problem would be solved very simply in two cases, if:
- each settlement was associated with only one type of transport, it was possible to use the usual polymorphic associations ;
- there was only one type of transport, then many-to-many associations could be used.
But in this example it is necessary to use the third method, which includes the capabilities of both methods.
')

Non-optimal solution


The first thing that comes to mind is to create four service transitive tables that will combine each type of transport with settlements.
class Train < ActiveRecord::Base has_many :train_locations, dependent: :destroy has_many :locations, through: :train_locations end class TrainLocation < ActiveRecord::Base belongs_to :train belongs_to :location end 

View full code

And the Location class, which refers to all 4 modes of transport.
 class Location < ActiveRecord::Base has_many :train_locations, dependent: :destroy has_many :ship_locations, dependent: :destroy has_many :copter_locations, dependent: :destroy has_many :truck_locations, dependent: :destroy has_many :trains, :through => :train_locations has_many :ships, :through => :ship_locations has_many :copters, :through => :copter_locations has_many :trucks, :through => :truck_locations end 

Ufff ... It seems that there are 9 tables, 9 models and a bunch of homogeneous code. Doesn't that seem too much to implement one connection? And if there are 10 types of transport, you need 21 tables and 21 models for implementation?
Why not try using polymorphism in a single transitive table?
No sooner said than done!

Preliminary decision


Create a migration:
 class CreateMoveableLocations < ActiveRecord::Migration def change create_table :moveable_locations do |t| t.references :location t.references :moveable, polymorphic: true t.timestamps end end end 

Yes, I understand that moveable is not the most successful name, but it is better than transportable.

Next, create a class to store the associations:
 class MoveableLocation < ActiveRecord::Base belongs_to :location belongs_to :moveable, polymorphic: true end 

Create classes for modes of transport:
 class Train < ActiveRecord::Base has_many :moveable_locations, as: :moveable, dependent: :destroy has_many :locations, through: :moveable_locations end 

View full code

The as parameter is mandatory here, it tells the Train class that the connection is polymorphic.
And reduce the location
 class Location < ActiveRecord::Base has_many :moveable_locations, dependent: :destroy has_many :trains, :through => :moveable_locations has_many :ships, :through => :moveable_locations has_many :copters, :through => :moveable_locations has_many :trucks, :through => :moveable_locations end 

We run tests (after all, everyone writes tests for models, right?) And ... they do not pass.

Optimal solution


The fact is that there is still a little bit of special magic that will explain to the Location class the correspondence of associations (trains, ships etc) to the values ​​in the column moveable_type .
 class Location < ActiveRecord::Base has_many :moveable_locations, dependent: :destroy with_options :through => :moveable_locations, :source => :moveable do |location| has_many :trains, source_type: 'Train' has_many :ships, source_type: 'Ship' has_many :copters, source_type: 'Copter' has_many :trucks, source_type: 'Truck' end end 

The with_options block here only reduces the amount of code and does not write : through =>: moveable_locations,: source =>: moveable after each association is declared.
source and source_type are the parameters that will magically associate the Location with all modes of transport (I met the statement that source_type is a replacement for the class_name parameter, but this is not quite true, source_type is used only for polymorphic associations) .
Now we can conveniently work with entities in this way:
 train = Train.new train.locations << city1 train.locations << city2 train.locations << city3 copter = Copter.new copter.locations << city1 

And even so:
 big_city = Location.new big_city.trains << train1 big_city.trains << train2 big_city.copters << copter1 big_city.trucks << truck1 big_city.trucks << truck2 

As a result, we needed only one additional table and one additional model to implement a polymorphic transitive relationship.
View full code

PS:
Two lines in the means of transport:
  has_many :moveable_locations, as: :moveable, dependent: :destroy has_many :locations, through: :moveable_locations 

are common to all four classes, so they can be removed in a common plug-in

Source: https://habr.com/ru/post/208794/


All Articles