Наверняка многие сталкиваются с проблемой, когда есть несколько моделей, скажем, Client, Manager и User – у которых ряд полей – к примеру, name, email, position – одинаковые. При этом каждая из моделей обладает также уникальными полями и методами. В данном случае (рассуждая абстрактно) логично было бы общие поля с соответствующими валидациями вынести в отдельную таблицу people (модель Person), оставив в Client, Manager и User только специфику.
Ряд примеров можно продолжать: 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 вполне применим при должном покрытии тестами.
Сталкивались ли вы с похожей проблемой? Известно ли вам о других вариантах решения? Буду рад услышать ваше мнение в комментариях.
Автор: rastarobot