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!
Лучшая благодарность автору — ваши комментарии! (а по желанию можно плюсануть меня на )
июля 26, 2011 at 19:42 (#)
[Использую STI можно также отказаться...]
… Используя
[Вместо type я предлагаю использовать поле с префиксоом...]
… префиксом
Поправьте пожалуйста.
За статью спасибо.
июля 26, 2011 at 20:22 (#)
rezwyi, спасибо за найденные ошибки, сейчас поправлю.
июля 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])
июля 27, 2011 at 11:59 (#)
Михаил, вы правы,однако зачем специально переопределять имя ответственного за STI поля, если имеется type, а в случаях, когда вам нужно хранить тип какой-то сущности можно использовать имя поля с синонимом. Так на ум даже не приходит вариантов, когда переопределение может пригодиться.
июля 27, 2011 at 14:56 (#)
Вам повезло.
У меня вот сейчас в разработке морда к чужой базе, и поле type там есть в двух таблицах. А сделать с этим ничего нельзя.
И не Михаил, а Максим ;)
июля 27, 2011 at 18:05 (#)
На самом то деле поле :type используеться для STI «по-умолчанию», это поведение можно легко поменять через self.inheritance_column = :klass
STI довольно точно характеризует технологию и слудует это понимать не как «наследование талиц», а «наследование на базе одной таблицы»
Следовало так же упомянуть об ограничениях: все актребуты доступны всем классам иерархии и как следствие нельзя переопределить тип атребута в наследнике.
июля 28, 2011 at 01:10 (#)
Спасибо за уточнения, Роман.
Максим, прошу прощения, чего-то я попутал. Спасибо за пример.
августа 1, 2011 at 19:06 (#)
Друзья не знаю кому обратиться занимаюсь продвижением сайта на Ruby robots.txt полностью закрывает сайт от индексации. А реально сайт индексируются и позиции меняются. Если кто знает подскажите пожалуйста как правильно настраивать этот файл для Ruby
августа 1, 2011 at 19:23 (#)
dimas, прощаю за оффтоп. Не хочу хвастаться своим google-fu, но вот, что я нашел: . Вообще не понял, что вы хотели написать в комментарии? Сайт индексируется или нет? Что за «позиции меняются»?
января 4, 2012 at 13:54 (#)
Пытаюсь сейчас продумать свое приожение. Столкнулся с похожей проблемой.Что-то я запутлася. Это получается у каждого продукта будет по 2 модели(сам продукт и attr)? А если продуктов много. Это моделей будет больше сотни,
января 4, 2012 at 14:23 (#)
tankard, как вариант можно отказаться от STI и использовать шаблоны. Шаблон — это модель, которая has_many :attrs. Все продукты имеют базовые свойства: название, цена, описание + свойства объявленные в шаблоне + собственные уникальные свойства (Product has_many :attrs) — примерно так это организовано в Spree и вроде бы в RoReCommerce, если не ошибаюсь.
января 4, 2012 at 15:09 (#)
Хм…теперь я еще больше запутался)) У меня не продукты. У меня недвижимость. Есть разные типы недвижимости: дом, квартира, гараж итд. У всех есть общие атрибуты(цена, адрес, описание итд) и есть уникальные атрибуты. Как такое лучше реализовать?
января 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, например, если не ошибаюсь используются шаблоны в виде моделей, нужен новый тип товара — создаешь модель со специфическими свойствами этого типа товара.
января 7, 2012 at 11:23 (#)
А STI не лучше?
января 7, 2012 at 13:56 (#)
При STI все равно будут нужны шаблоны. STI удобно только тем, что мы не должны писать тип в where и при создании новой записи, но нужно будет создавать по моделе на каждый тип недвижимости.
Писать везде where или просто создать пустые модели, которые не нужно тестировать — вот между чем выбор, если у каждого типа недвижимости нет уникального поведения. Если необходимо реализовать уникальное поведение для каждой модели, но красивее будет использовать STI.
января 7, 2012 at 20:34 (#)
Перечитал статью еще раз. Я не понимаю зачем для уникальных атрибутов создавать отдельные таблицы и модели? Можно же хранить все в одной таблице. Или я что-то упускаю?
января 17, 2012 at 19:10 (#)
[...] Consultor использует STI (Single Table Inheritance) — наследуется от модели User, так как консультант это [...]
августа 29, 2012 at 23:43 (#)
А какая разница между STI и полиморфными связями?
Заранее спасибо!
августа 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, а еще код в целом становится лаконичнее и удобнее для понимания.
июня 28, 2013 at 07:32 (#)
У вас опечатка — «Frigde». Я еще долго думал что это за слово такое… )
А статься замечательная, спасибо! Начал сейчас смотреть railscasts по этому вопросу, но захотелось все же на родном языке об этом прочитать. Спасибо еще раз :)