RDR3R > Single Table Inheritance (STI) в Rails 3

июля 26, 2011  |  Published in Ruby on Rails, Ruby on Rails 3  |  20 Comments

Пришло мне тут два письма с просьбой написать о STI — наследовании от одной таблицы. Материал достаточно простой, однако, если люди просят — почему не уделить написанию статьи пол часика?

Признаться честно, мне не нравится термин STI применительно к Rails, поскольку на самом деле наследуются не таблицы, а классы. Здесь больше подходит термин Class Table Inheritance поскольку один класс наследуясь от другого получает доступ к таблице основного класса.

Разберемся с терминами.

Базовый класс — это класс от которого наследуются все остальные классы.

Наследуемый класс — здесь все понятно.

Для чего необходимо STI? — STI достаточно удобная штука и используется в двух основных случаях:

1. У нас имеются две и более моделей, которые имеют абсолютно одинаковые атрибуты, но должны вести себя по разному.

2. У нас имеются две и более моделей, которые имеют несколько общих атрибутов, общие атрибуты выносятся в отдельную таблицу и отдельную модель — базовый класс.

Примеры, где может использоваться STI

Представьте, что разрабатываете интернет магазин. В интернет магазине имеются два места, где можно использовать STI — при работе с товарами и при работе с пользователями.

Начнем с пользователе. Пользователи могут быть трех типов: Supplier, Employee и Customer. Все эти пользоователи имеют набор общих свойств:
имя, фамилия, отчество, логин, пароль, дата регистрации, дата последнего визита. Здесь STI можно использовать при хрранении этих общих данных:

class User < ActiveRecord::Base
end

class Customer < User
end

class Employee < User
end

class Supplier < User
end

Модели Customer, Employee и Supplier используют одну и ту же таблицу — users.

Теперь  посмотрим на товары, все они имеют свойства: name, quantity, price, currency, sales_count, profit_count, description, rate_count.  Кроме общих свойств (атрибутов) продукты могут иметь также свои уникальные атрибуты, например у холодильников это объем, у телевизоров — диагональ, у процессоров — частота, у автомобилей — время разгона до 100 км./ч.. Все эти отличные атрибуты необходимо вынести в отдельные таблицы и соответствующие им модели, например: FridgeAttr, CarAttr, CpuArrt, TvAttr и т.д., которые потом ассоциировать с моделями Frigde, Car, Cpu, Tv и т.д.

class Product < ArctiveRecord::Base
end

class Fridge < Product
  has_one :fridge_attr, :dependent => :destroy
end

Теперь базовые свойства холодильника хранятся в таблице products, а все общие свойства холодильников хранятся в таблице fridge_attrs.

Реализация STI в Rails 3

STI — заключается не только в наследовании одной модели от другой, для того, чтобы STI заработало, таблица, что ассоциируется с базовой моделью должна содержать поле type. Именно поле type определяет то, какому наследуемому классу принадлежит запись.

В случае с холодильников поле type будет содержать «Fridge», в случае с автомобилем — «Car» и т.д. Используя STI можно также отказаться от создания модели Category для сортирования товаров по типу, вам просто следует использовать запросы с .where, или .find_by_type.

Внимание! Поле с названием type зарезирвировано специально для STI, по этому вы не можете его использовать в других целях ибо у вас будут появляться ошибки. Вместо type я предлагаю использовать поле с префиксом — именем модели, для User, это будет, user_type, для Chair — chair_type и т.д.

Теперь вы знаете, что такое STI.

Удачи с изучением Rails!

Лучшая благодарность автору — ваши комментарии! (а по желанию можно плюсануть меня на )

Tags: , , ,

Responses

  1. rezwyi says:

    июля 26, 2011 at 19:42 (#)

    [Использую STI можно также отказаться...]
    … Используя

    [Вместо type я предлагаю использовать поле с префиксоом...]
    … префиксом

    Поправьте пожалуйста.

    За статью спасибо.

  2. admin says:

    июля 26, 2011 at 20:22 (#)

    rezwyi, спасибо за найденные ошибки, сейчас поправлю.

  3. Maxim Filatov says:

    июля 27, 2011 at 10:18 (#)

    [quote] type зарезирвировано специально для STI, по этому вы не можете[/quote]

    Можете-можете:

    Active Record allows inheritance by storing the name of the class in a column that by default is named “type” (can be changed by overwriting [b]Base.inheritance_column[/b])

  4. admin says:

    июля 27, 2011 at 11:59 (#)

    Михаил, вы правы,однако зачем специально переопределять имя ответственного за STI поля, если имеется type, а в случаях, когда вам нужно хранить тип какой-то сущности можно использовать имя поля с синонимом. Так на ум даже не приходит вариантов, когда переопределение может пригодиться.

  5. Maxim Filatov says:

    июля 27, 2011 at 14:56 (#)

    Вам повезло.
    У меня вот сейчас в разработке морда к чужой базе, и поле type там есть в двух таблицах. А сделать с этим ничего нельзя.

    И не Михаил, а Максим ;)

  6. says:

    июля 27, 2011 at 18:05 (#)

    На самом то деле поле :type используеться для STI «по-умолчанию», это поведение можно легко поменять через self.inheritance_column = :klass

    STI довольно точно характеризует технологию и слудует это понимать не как «наследование талиц», а «наследование на базе одной таблицы»

    Следовало так же упомянуть об ограничениях: все актребуты доступны всем классам иерархии и как следствие нельзя переопределить тип атребута в наследнике.

  7. admin says:

    июля 28, 2011 at 01:10 (#)

    Спасибо за уточнения, Роман.

    Максим, прошу прощения, чего-то я попутал. Спасибо за пример.

  8. dimas says:

    августа 1, 2011 at 19:06 (#)

    Друзья не знаю кому обратиться занимаюсь продвижением сайта на Ruby robots.txt полностью закрывает сайт от индексации. А реально сайт индексируются и позиции меняются. Если кто знает подскажите пожалуйста как правильно настраивать этот файл для Ruby

  9. admin says:

    августа 1, 2011 at 19:23 (#)

    dimas, прощаю за оффтоп. Не хочу хвастаться своим google-fu, но вот, что я нашел: . Вообще не понял, что вы хотели написать в комментарии? Сайт индексируется или нет? Что за «позиции меняются»?

  10. tankard says:

    января 4, 2012 at 13:54 (#)

    Пытаюсь сейчас продумать свое приожение. Столкнулся с похожей проблемой.Что-то я запутлася. Это получается у каждого продукта будет по 2 модели(сам продукт и attr)? А если продуктов много. Это моделей будет больше сотни,

  11. admin says:

    января 4, 2012 at 14:23 (#)

    tankard, как вариант можно отказаться от STI и использовать шаблоны. Шаблон — это модель, которая has_many :attrs. Все продукты имеют базовые свойства: название, цена, описание + свойства объявленные в шаблоне + собственные уникальные свойства (Product has_many :attrs) — примерно так это организовано в Spree и вроде бы в RoReCommerce, если не ошибаюсь.

  12. tankard says:

    января 4, 2012 at 15:09 (#)

    Хм…теперь я еще больше запутался)) У меня не продукты. У меня недвижимость. Есть разные типы недвижимости: дом, квартира, гараж итд. У всех есть общие атрибуты(цена, адрес, описание итд) и есть уникальные атрибуты. Как такое лучше реализовать?

  13. admin says:

    января 5, 2012 at 13:52 (#)

    tankard, я бы так сделал:

    1. Есть базовая модель «Недвижимость» (не знаю как оно по английски =)) и она has_one :template

    2. Модель Template содержит особые поля определенного типа недвижимости, например, количество комнат, которые могут быть у квартиры или коттеджа, но не могут буть у гаража. Недвижимость + Flat = сущность Flat. То есть в недвижимость мы просто выносим общие поля.

    3. Модель Props — хранит какие-то дополнительные свойства, которые вообще уникальны могут быть, для какой-то конкретной записи о недвижимости, например владелец захотел указать, у него в саду стоят амечательные домики, или, что дом удостоился какой-нибудь архитектурной награды.

    Недвижимость has_one :template
    Недвижимость has_many :props

    Как вариант, шаблоны могут быть не моделями, но просто списком коллекций полей определенных в базовой модели «Недвижимость».

    templates = {
    :flat => [
    {
    :name => :bedrooms_number,
    :type => :integer,
    :default => false
    },...],
    :garage => [...]
    }

    или так:

    templates = {
    :flat => [
    :bedrooms_number =>{
    :type => :integer,
    :default => false
    },...],
    :garage => [...]
    }

    Это по этому хэшу можно в форме добавления недвижимости выбрать тип недвижимости ajax’ом и дорисовать нужные поля формы. После передачи данных из формы, модель занимается созданием запизи в «Недвижимость» и записей в таблице props которые представляют отдельные свойства. Потом при выдаче пользователю «Недвижимость» и все соответствующие ей props склеиваются как бы в одну сущность.

    Таким образом можно обойтись всего двумя моделями: «Недвижимость» и Prop (если шаблоны в виде хэша).

    Недвижимость has_many :props (properties)

    Если вдруг потребуется добавить всем шаблонам новое поле, то это можно сделать добавив его в хеш, а потом пробежаться по всем ранее созданным записям недвижимости и создавать для них новую запись Prop которая будет хранить новое свойство с дефолным значением — для этого лучше всего будет использовать миграцию в которой будет внутри использоваться модель «недвижимость». Этот подход мне кажется лучше, чем с моделями для каждого шаблона, хотя для коробочных решений я бы его не использовал, в Spree, например, если не ошибаюсь используются шаблоны в виде моделей, нужен новый тип товара — создаешь модель со специфическими свойствами этого типа товара.

  14. tankard says:

    января 7, 2012 at 11:23 (#)

    А STI не лучше?

  15. admin says:

    января 7, 2012 at 13:56 (#)

    При STI все равно будут нужны шаблоны. STI удобно только тем, что мы не должны писать тип в where и при создании новой записи, но нужно будет создавать по моделе на каждый тип недвижимости.

    Писать везде where или просто создать пустые модели, которые не нужно тестировать — вот между чем выбор, если у каждого типа недвижимости нет уникального поведения. Если необходимо реализовать уникальное поведение для каждой модели, но красивее будет использовать STI.

  16. tankard says:

    января 7, 2012 at 20:34 (#)

    Перечитал статью еще раз. Я не понимаю зачем для уникальных атрибутов создавать отдельные таблицы и модели? Можно же хранить все в одной таблице. Или я что-то упускаю?

  17. says:

    января 17, 2012 at 19:10 (#)

    [...] Consultor использует STI (Single Table Inheritance) — наследуется от модели User, так как консультант это [...]

  18. Alexandr says:

    августа 29, 2012 at 23:43 (#)

    А какая разница между STI и полиморфными связями?
    Заранее спасибо!

  19. admin says:

    августа 30, 2012 at 07:30 (#)

    STI — все сущности родственны, например: Admin, Moderator, Reader < User. Каждая модель использует таблицу users, но модели Admin, Moderator и Reader могут иметь собственный код.

    Полиморфные ассоциации — ассоциации в которых одна сущность принадлежит многим, например, Comments belongs_to Article, Topic, Product и т.д. В comments могут присутствовать поля article_id, topic_id, product_id, в Comment должна быть более сложная валидация, так как обязательной должна быть одна из ссылок, причем вообще быть должна только одна, или article_id, или topic_id, или product_id. Чтобы соптимизировать это используются полиморфные ассоциации, где Comments belongs_to Commentable, а Commentable — любая сущность. Валидация становится проще, выборку комментариев определенного типа, если нужно, можно делать по commentable_type, а еще код в целом становится лаконичнее и удобнее для понимания.

  20. says:

    июня 28, 2013 at 07:32 (#)

    У вас опечатка — «Frigde». Я еще долго думал что это за слово такое… )
    А статься замечательная, спасибо! Начал сейчас смотреть railscasts по этому вопросу, но захотелось все же на родном языке об этом прочитать. Спасибо еще раз :)

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.