...

воскресенье, 12 января 2014 г.

[Из песочницы] Полиморфные сквозные ассоциации в Ruby on Rails

В статье идет речь о методе создания полиморфизма для связей many-to-many в Ruby on Rails.

Задача




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

Каждый вид транспорта в разрабатываемой системе будет представлен своим классом: 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




являются общими для всех четырех классов, поэтому их можно убрать в общий подключаемый модуль

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий