Наверняка многие сталкиваются с проблемой, когда есть несколько моделей, скажем, 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 вполне применим при должном покрытии тестами.

Сталкивались ли вы с похожей проблемой? Известно ли вам о других вариантах решения? Буду рад услышать ваше мнение в комментариях.

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


  1. sl_bug
    18.06.2015 17:49

    Описано в Rails Edge Guides и пока что отсутствует в стабильной версии.


    Какого года статья? 2008-2009?


    1. rastarobot Автор
      18.06.2015 18:01

      2015. Пруф:
      guides.rubyonrails.org/association_basics.html
      edgeguides.rubyonrails.org/association_basics.html
      Или оно уже есть из коробки, просто в гайдах не описано?


      1. sl_bug
        18.06.2015 18:05

        Оно еще во 2-й версии было. Возможно в гайдах и не было.


        1. rastarobot Автор
          18.06.2015 18:19

          Спасибо, поправил.


  1. sl_bug
    18.06.2015 17:55
    +2

    Основная суть такая. Используем STI если набор аттрибутов у каждого объекта примерно одинаков, но объекты имеют разное поведение. В других случаях ваш второй вариант.


    1. sl_bug
      18.06.2015 18:06

      Ну а если уж совсем MTI охота, то берем gem Sequel. В нем есть — Sequel::Plugins::ClassTableInheritance (гем не совсем для этих целей, но умеет и это)

      Или github.com/hzamani/acts_as_relation (но там багов хватает)


      1. rastarobot Автор
        18.06.2015 18:26

        Собственно, упомянутый active_record-acts_as – это продолжение заброшенного acts_as_relation, как следует из readme на Github. Что касается Sequel – да, намного более матёрый гем чем active_record-acts_as, посмотрю, спасибо.


  1. estum
    20.06.2015 06:18

    Если юзается Postgres, то можно вынести частные поля в колонку с типом jsonb. Даже отношения можно замутить, если сделать View для каждой под-модели + в этом случае не нужно будет поле type. Всего пара костылей в абстракции, чтобы все работало как надо. Стильно, модно, молодежно, но особо никаких профитов по производительности, делаю так только чтобы ради единственной связи несколько отдельных таблиц не создавать.


  1. FanKiLL
    20.06.2015 17:40

    А как же полиморфизм? Хабр не даёт вставлять код из-за кармы, вот ссылка на код
    pastebin.com/VZScxira