Перед тем как начать повествование, вспомним что же такое STI.
STI (Single Table Inheritance) — паттерн проектирования, который позволяет перенести объектно-ориентированное наследование на таблицу реляционной базы данных. В таблице БД должно присутствовать поле идентифицирующее название класса в иерархии. Зачастую, в том числе в RoR, поле называют type.
С помощью данного паттерна можно создавать объекты, которые содержат идентичный набор полей, но имеют разное поведение. Например, таблица пользователей, содержащая имя, логин и пароль, но использовалось два класс пользователей Admin, Visitor. Каждый класс, содержит как унаследованны так и индивидуальный набор методов. Определение того, какой класс будет создан и используется поле type, имя поля может быть переопределено.
Таким образом, если рассматривать канонический случай: имена классов хранятся в одной таблице с данными.

Но может случится иная ситуация…
Бывают задачи, когда необходимо поверх существующей базы, на которую уже много чего завязано сделать некоторый web-редактор. И вероятность того, что имеющаяся схема будет полностью удовлетворять требованиям ORM мала. Как следствие приходится, конфигурируя модели, натягивать все это дело.

Довольно распространенной практикой, для нормализации, является использование справочных таблиц.
Например это может быть таблица контактов связанная со справочником типов контактов. При этом было бы логично сделать проверку введенных данных на уровне модели, можно добавить методы для форматирования значений и так далее.
Для решения этой задачи есть два пути:
Второй вариант даже не рассматриваю, т.к. является слишком громоздким и не слишком гибким. Поэтому остановимся на первом.
И так, для использования STI необходимо дополнительное поле, которое будет указывать на класс. Переделывать схему можно, но возрастает избыточность, которую нужно поддерживать в корректном состоянии. В случае приведенного примера, при добавлении поля type, придется значение поля синхронизировать с внешним ключом. Поэтому было бы логично воспользоваться имеющимися данными. Т.к. определение имени класса происходит еще до его создания, то вмешиваться придется в работу самого ActiveRecord.
Копания в документации и исходниках прояснили весь этот механизм. За него отвечает метод instantiate находящийся в модуле ActiveRecord::Inheritance:
Данный метод достаточно прост:
Рассмотрим как же определяется какой класс должен быть создан. Для этого смотрим исходники дальше, а именно метод find_sti_class, в который передается имя типа взятого из поля соответствующего inheritance_column, по-умолчанию, как уже было сказано ранее, оно равно type.
Как видите нет какой-то особой магии. Поэтому для решения поставленной задачи было необходимо переопределить метод instantiate, чтобы вместо значения из поля передавалось другое, полученное из связанной таблицы.
Полученное решение было оформленно в виде Gem-a. Работает по такому же принципу как и ассоциации. ActiveRecord расширяет дополнительным методом acts_as_ati, который имеет такой же синтаксис как и метод belongs_to.
В данном методе формируется хэш со вспомогательной информацией по связи, также добавляется собственно само отношение и валидаторы. Кроме того экземпляр расширяется рядом вспомогательных методов + собственно выполняется перегрузка.
Перегруженный метод в основе своей не меняется, добавляется лишь получение на основании созданного отношения имени класса.
На этом основные изменения заканчиваются. Используя полученный класс получилось реализовать STI через связанную таблицу. Данный подход имеет минус по производительности (местами решено кэшированием данных), но при этом дает возможность в полной мере использовать полиморфизм.
Пример использования:
Данное решение применяется в работе внутреннего ресурса и пока показало себя только с положительной стороны и позволило сделать более читабельный и простой в поддержке код.
Gem пока еще не размещен на rubygems, но его можно подключить через Gemfile:
либо как локальную копию
STI (Single Table Inheritance) — паттерн проектирования, который позволяет перенести объектно-ориентированное наследование на таблицу реляционной базы данных. В таблице БД должно присутствовать поле идентифицирующее название класса в иерархии. Зачастую, в том числе в RoR, поле называют type.
С помощью данного паттерна можно создавать объекты, которые содержат идентичный набор полей, но имеют разное поведение. Например, таблица пользователей, содержащая имя, логин и пароль, но использовалось два класс пользователей Admin, Visitor. Каждый класс, содержит как унаследованны так и индивидуальный набор методов. Определение того, какой класс будет создан и используется поле type, имя поля может быть переопределено.
Таким образом, если рассматривать канонический случай: имена классов хранятся в одной таблице с данными.

Но может случится иная ситуация…
Бывают задачи, когда необходимо поверх существующей базы, на которую уже много чего завязано сделать некоторый web-редактор. И вероятность того, что имеющаяся схема будет полностью удовлетворять требованиям ORM мала. Как следствие приходится, конфигурируя модели, натягивать все это дело.

Довольно распространенной практикой, для нормализации, является использование справочных таблиц.
Например это может быть таблица контактов связанная со справочником типов контактов. При этом было бы логично сделать проверку введенных данных на уровне модели, можно добавить методы для форматирования значений и так далее.
Для решения этой задачи есть два пути:
- воспользоваться STI, он прямо напрашивается сюда;
- использовать один толстый класс, в котором логику определять через case.
Второй вариант даже не рассматриваю, т.к. является слишком громоздким и не слишком гибким. Поэтому остановимся на первом.
И так, для использования STI необходимо дополнительное поле, которое будет указывать на класс. Переделывать схему можно, но возрастает избыточность, которую нужно поддерживать в корректном состоянии. В случае приведенного примера, при добавлении поля type, придется значение поля синхронизировать с внешним ключом. Поэтому было бы логично воспользоваться имеющимися данными. Т.к. определение имени класса происходит еще до его создания, то вмешиваться придется в работу самого ActiveRecord.
Копания в документации и исходниках прояснили весь этот механизм. За него отвечает метод instantiate находящийся в модуле ActiveRecord::Inheritance:
# File activerecord/lib/active_record/inheritance.rb, line 61 def instantiate(record) sti_class = find_sti_class(record[inheritance_column]) record_id = sti_class.primary_key && record[sti_class.primary_key] if ActiveRecord::IdentityMap.enabled? && record_id instance = use_identity_map(sti_class, record_id, record) else instance = sti_class.allocate.init_with('attributes' => record) end instance end
Данный метод достаточно прост:
- определяется класс который должен быть создан;
- если включена поддержка IdentityMap, то используем ее, иначе формируем новый экземпляр на основании полученных из базы данных.
Рассмотрим как же определяется какой класс должен быть создан. Для этого смотрим исходники дальше, а именно метод find_sti_class, в который передается имя типа взятого из поля соответствующего inheritance_column, по-умолчанию, как уже было сказано ранее, оно равно type.
Как видите нет какой-то особой магии. Поэтому для решения поставленной задачи было необходимо переопределить метод instantiate, чтобы вместо значения из поля передавалось другое, полученное из связанной таблицы.
Полученное решение было оформленно в виде Gem-a. Работает по такому же принципу как и ассоциации. ActiveRecord расширяет дополнительным методом acts_as_ati, который имеет такой же синтаксис как и метод belongs_to.
@association_inheritance = { id: 0, field_name: params[:field_name] || :name, block: block_given? ? Proc.new {|type| yield type } : Proc.new{ |type| type }, class_cache: {}, alias: {} } params.delete :field_name @association_inheritance[:association] = belongs_to(association_name, params) validates @association_inheritance[:association].foreign_key.to_sym, :presence => true before_validation :init_type
В данном методе формируется хэш со вспомогательной информацией по связи, также добавляется собственно само отношение и валидаторы. Кроме того экземпляр расширяется рядом вспомогательных методов + собственно выполняется перегрузка.
Перегруженный метод в основе своей не меняется, добавляется лишь получение на основании созданного отношения имени класса.
params = self.association_inheritance class_type = if record.is_a? String (params[:alias][record.to_s.downcase.to_sym] || record).to_s.classify else association = params[:association] type_id = record[association.foreign_key.to_s] params[:class_cache][type_id] ||= begin inheritance_record = association.klass.find(type_id) value = inheritance_record.send(params[:field_name].to_sym) value = (params[:alias][value.to_s.downcase.to_sym] || value) value.to_s.classify rescue ::ActiveRecord::RecordNotFound '' end end sti_class = find_sti_class(params[:block].call(class_type))
На этом основные изменения заканчиваются. Используя полученный класс получилось реализовать STI через связанную таблицу. Данный подход имеет минус по производительности (местами решено кэшированием данных), но при этом дает возможность в полной мере использовать полиморфизм.
Пример использования:
class PostType < ActiveRecord::Base end class Post < ActiveRecord::Base attr_accessible :name acts_as_ati :type, :class_name => PostType, :foreign_key => :post_type_id, :field_name => :name do |type| "#{type}Post" end end class ForumPost < Post attr_accessible :name ati_type :forum end class BlogPost < Post attr_accessible :name ati_type :blog end
Данное решение применяется в работе внутреннего ресурса и пока показало себя только с положительной стороны и позволило сделать более читабельный и простой в поддержке код.
Gem пока еще не размещен на rubygems, но его можно подключить через Gemfile:
gem 'ext_sti', :git => 'git://github.com/fuCtor/ext_sti.git'либо как локальную копию
gem 'ext_sti', :path => %path_to_ext_sti%