Пользователь
0,0
рейтинг
12 января 2014 в 18:32

Разработка → Полиморфные сквозные ассоциации в 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

являются общими для всех четырех классов, поэтому их можно убрать в общий подключаемый модуль
@paranoic
карма
7,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (18)

  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Примеры фактори:

      FactoryGirl.define do
        factory :truck do
        end
      
        factory :ship do
        end
      
        factory :location do
          trait :with_truck do
            after(:build) { |l| l.trucks << FactoryGirl.build(:truck) }
          end
      
          trait :with_ship do
            after(:build) { |l| l.ships << FactoryGirl.build(:ship) }
          end
        end
      end
      
  • +4
    извините, я не понимаю, чем тут не работает многие-ко-многим?
    • –1
      просто Depricated. Плюс при has_many -> has_many в соеденительной таблице можно сохранить доп. аттрибуты.
    • 0
      мне тоже неясно
    • 0
      has_and_belongs_to_many по сути использует промежуточную таблицу. Что в результате выльется в 4 таблицы trains_locations, ships_locations, copters_locations, trucks_locations
      • 0
        и что с того?
        само по себе увеличение количества таблиц не есть плохо. иначе все пихали бы в одну большую таблицу.
        действительно, код с полиморфными связями несколько более элегантен (абстрактно), но с практической точки зрения это ничего не дает. с точки зрения производительности, непонятно, что же лучше, связи многие-ко-многим или или полиморфные связи в данном случае. некоторое отклонение от DRY может быть очень даже к месту.

        я бы обошелся в данном конкретном варианте именно n-to-n связми, поскольку, как мне кажется, у этих таблицу будут физически разные пользователи, которые будут нагружать эти таблицы неравномерно (вероятно). а значит, когда вопрос коснется оптимизации, кеширования и настройки производительности, вам будет легче это делать с разными таблицами, а не с одной. в т.ч. разносить их физически и даже делить таблицу location на четыре части, если потребуется.
        в противном случае, если мое допущение неверно, я бы сделал STI, как правильно отметили ниже.

        конечно, все это сделать можно и с полиморфными связями, нет ничего не возможного, но человеко-часы не бесплатны и не бесконечны.
  • +5
    Для выше приведенного кейса, когда выделяются классы TrainLocation, CopterLocation и т.д., удобней использовать STI. Так как реляционные БД не поддерживают наследование, а хранение каждого класса в отдельной таблице увеличивате количество JOIN`ов, используется паттерн «Наследование с единой таблицей» — поля всех классов иерархии записываются в одну таблицу.
    • 0
      Плюс вам. Более того тут возможно (сильно зависит от контекста, но в данном случае вполне) использовать STI и для самих типов транспорта. Это бы все упростило.
      • 0
        Если модели видов транспорта будут иметь множество разношерстных атрибутов (уникальных для каждой модели), то sti — уже будет неудачным вариантом.
  • +1
    Если бы я решал задачу, то скорее всего сделал бы как вы, но:
    1) Вместо MoveableLocation попытался бы выделить какие-то общие характеристики из всех типов транспорта и получил бы сущность Carrier. Эта сущность как раз бы и имела полиморфную связь на Train, Copter и так далее.

    2) Если общие характеристики выделить не получается или их слишком мало, то действительно нужно воспользоваться STI (как уже писали выше).
  • 0
    Хорошее решение, пример конечно можно решить и через has_many through. Но на самом деле бывают другие причины использования сквозных ассоциаций с полиморфными связями. Я из этого поста извлек пользу того как можно сделать has_many through если source полиморфная ассоциация. Спасибо за статью!
  • 0
    Схема красивая. В Фотошопе рисовалась или есть какая-то чудесная прога, которая умеет такие схемы конструировать?
    • 0
      Dia

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.