Ряд примеров можно продолжать: Product – Fridge, Phone, Toaster; Vehicle – Car, Truck, Motorcycle и так далее. Проблема довольно общая – какие варианты решения есть для Rails?
Варианты
Single Table Inheritance (STI)
Об этом много написано, в частности в Rails Guides и на Хабре тут и тут. Суть в том, что записи для Client, Manager и User мы помещаем в одну таблицу people, используя специальное поле type для того, чтобы было понятно, «кто есть кто». Вот пример; он условный, общие поля помечены звёздочкой, частные – заглавными буквами названий моделей:
type | name* | email* | position (M) | company (C) | hobby (U)
-----------------------------------------------------------------------------------------
manager | Slartibartfast | a@b.com | Planet designer | NULL | NULL
client | Ford | c@d.com | | Megadodo Publications | NULL
user | Arthur | e@f.com | NULL | NULL | Travelling
Однако, в этом случае Manager приобретает несвойственное ему свойство hobby от User. Также, поле hobby менеджеров всегда будет пустовать. User, Client и Manager в данном случае являются подклассами Person, не имеющие своих собственных таблиц, и каждое уникальное свойство нужно объявлять в родительской таблице/модели.
В принципе, на это можно было бы закрыть глаза, но что, если Manager требует создания 42 собственных полей, не имеющих никакого отношения к Client и User? В этом случае было бы логичнее перенести специфичные поля в отдельные таблицы clients, users и managers, оставив в people только общие поля, а также type и id для построения нужных связей. Такая схема, как подсказывает Google, называется Multiple Table Inheritance, но, к большому сожалению, Rails о ней пока ничего не знает, и, как показывает беглый поиск по форуму разработчиков, в обозримом будущем не собирается.
Nested attributes + Delegation
Проблему можно решить и так:
class Manager < ActiveRecord::Base
belongs_to :person
# общее
accepts_nested_attributes_for :person
delegate :name, :email, to: :person
# частное
validates_presence_of :position
end
Company.find(42).managers.create(position: 'Paranoid android', person_attributes: {
name: 'Marvin', email: 'whats_the@point_to.be'
})
В принципе вариант, но в данном случае придётся всё время выделять общие атрибуты в отдельный хэш при создании или модификации записи, а также пользоваться хелпером fields_for при выводе форм, а также делать дополнительные приседания в контроллере и модели, о чём подробно написано в тех же Rails Guides.
Хочется же «бесшовного» слияния между обоими моделями и полной изоляции частностей реализации общих полей Manager, User и Client с точки зрения других классов приложения.
Свой огород
По понятным причинам, городить его совсем не хочется, хотя, если поискать по сочетанию «rails multiple table inheritance» или «rails class table inheritance», то найдётся множество вариантов собственноручной реализации MTI.
Gems
Логично было с самого начала предположить, что всё уже сделано до нас, и вот что мне удалось найти. Не очень подробное изучение репозиториев на Github показывает, что живёт и развивается из них только active_record-acts_as. Не буду дублировать Readme, в нём достаточно наглядно описано, как пользоваться гемом. Беглый взгляд на issues показывает, что проект ещё в начальной фазе, но IMO вполне применим при должном покрытии тестами.
Сталкивались ли вы с похожей проблемой? Известно ли вам о других вариантах решения? Буду рад услышать ваше мнение в комментариях.
Комментарии (9)
sl_bug
18.06.2015 17:55+2Основная суть такая. Используем STI если набор аттрибутов у каждого объекта примерно одинаков, но объекты имеют разное поведение. В других случаях ваш второй вариант.
sl_bug
18.06.2015 18:06Ну а если уж совсем MTI охота, то берем gem Sequel. В нем есть — Sequel::Plugins::ClassTableInheritance (гем не совсем для этих целей, но умеет и это)
Или github.com/hzamani/acts_as_relation (но там багов хватает)rastarobot Автор
18.06.2015 18:26Собственно, упомянутый active_record-acts_as – это продолжение заброшенного acts_as_relation, как следует из readme на Github. Что касается Sequel – да, намного более матёрый гем чем active_record-acts_as, посмотрю, спасибо.
estum
20.06.2015 06:18Если юзается Postgres, то можно вынести частные поля в колонку с типом jsonb. Даже отношения можно замутить, если сделать View для каждой под-модели + в этом случае не нужно будет поле type. Всего пара костылей в абстракции, чтобы все работало как надо. Стильно, модно, молодежно, но особо никаких профитов по производительности, делаю так только чтобы ради единственной связи несколько отдельных таблиц не создавать.
FanKiLL
20.06.2015 17:40А как же полиморфизм? Хабр не даёт вставлять код из-за кармы, вот ссылка на код
pastebin.com/VZScxira
sl_bug
Какого года статья? 2008-2009?
rastarobot Автор
2015. Пруф:
guides.rubyonrails.org/association_basics.html
edgeguides.rubyonrails.org/association_basics.html
Или оно уже есть из коробки, просто в гайдах не описано?
sl_bug
Оно еще во 2-й версии было. Возможно в гайдах и не было.
rastarobot Автор
Спасибо, поправил.