Задача
Допустим, что необходимо разработать систему управления грузовым транспортом. В нашем распоряжении имеются несколько видов этого транспорта: поезда, вертолеты, грузовики и баржи. И известно, что каждое средство осуществляет перевозку только в строго определенные населенные пункты. Например, часть грузовиков катается по центральной части России, часть по южной, вертолеты работают в Сибири и на Камчатке, поезда вообще ограничены железнодорожным полотном и так далее.
Каждый вид транспорта в разрабатываемой системе будет представлен своим классом: Train, Copter, Truck, Ship соответственно.
Населенные пункты (города, поселки, научные станции, тут нас интересует не размер, а географические координаты), куда осуществляется перевозка, представлены классом Location.
Стоит условие: к каждой единице транспорта может быть привязано сколько угодно Location. В свою очередь к каждому населенному пункту может быть привязано сколько угодно единиц транспорта разных видов.

Задача решалась бы очень просто в двух случаях, если бы:
— каждый населенный пункт был связан только с одним видом транспорта, то можно было использовать обычные полиморфные ассоциации;
— существовал только один вид транспорта, то можно было бы использовать ассоциации многое-ко-многому.
Но в данном примере необходимо использовать третий способ, который включает в себя возможности обоих методов.
Неоптимальное решение
Первое, что приходит в голову, создать четыре служебные транзитивные таблицы, которые будут объединять каждый вид транспорта с населенными пунктами.
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
И класс Location, который ссылается на все 4 вида транспорта
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
Уффф… Кажется тут получилось 9 таблиц, 9 моделей и куча однородного кода. Не кажется ли, что слишком много для реализации одной связи? А если будет 10 видов транспорта, то потребуется 21 таблица и 21 модель для реализации?
Почему бы не попробовать использовать полиморфизм в одной транзитивной таблице?
Сказано — сделано!
Предварительное решение
Создаем миграцию:
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
Да, я понимаю, что moveable — не самое удачное название, но оно лучше, чем transportable.
Далее, создаем класс для хранения ассоциаций:
class MoveableLocation < ActiveRecord::Base belongs_to :location belongs_to :moveable, polymorphic: true end
Создаем классы для видов транспорта:
class Train < ActiveRecord::Base has_many :moveable_locations, as: :moveable, dependent: :destroy has_many :locations, through: :moveable_locations end
Параметр as тут является обязательным, он говорит классу Train о том, что связь полиморфная.
И сокращаем 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
Запускаем тесты (ведь все пишут тесты для моделей, верно?) и… они не проходят.
Оптимальное решение
Дело в том, что тут еще нужно немного особой магии, которая объяснит классу Location соответствие ассоциаций (trains, ships etc) значениям в колонке 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
Блок with_options здесь всего лишь позволяет сократить количество кода и не писать :through => :moveable_locations, :source => :moveable после объявления каждой ассоциации.
source и source_type являются теми параметрами, которые магическим образом свяжут Location со всеми видами транспорта (я встречал утверждение, что source_type — это замена параметра class_name, но это не совсем верно, source_type используется только для полиморфных ассоциаций).
Теперь мы можем удобно работать с сущностями таким образом:
train = Train.new train.locations << city1 train.locations << city2 train.locations << city3 copter = Copter.new copter.locations << city1
И даже таким:
big_city = Location.new big_city.trains << train1 big_city.trains << train2 big_city.copters << copter1 big_city.trucks << truck1 big_city.trucks << truck2
В итоге для реализации полиморфной транзитивной связи нам потребовалась только одна дополнительная таблица и одна дополнительная модель.
Посмотреть код полностью
P.S.:
Две строчки в видах транспорта:
has_many :moveable_locations, as: :moveable, dependent: :destroy has_many :locations, through: :moveable_locations
являются общими для всех четырех классов, поэтому их можно убрать в общий подключаемый модуль
ссылка на оригинал статьи http://habrahabr.ru/post/208794/
Добавить комментарий