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 codeAnd 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 codeThe
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 codePS:
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