Before you start the story, remember what is the
STI .
STI (Single Table Inheritance) is a design pattern that allows you to transfer object-oriented inheritance to a relational database table. In the database table there should be a field identifying the name of the class in the hierarchy. Often, including in RoR, the field is called type.With this pattern, you can create objects that contain the same set of fields, but have different behaviors. For example, a user table containing a name, a login and a password, but two classes of users Admin, Visitor were used. Each class contains both an inherited and an individual set of methods. Determining which class will be created and the
type field is used, the field name can be overridden.
')
Thus, if we consider the canonical case: the class names are stored in the same table with the data.

But a different situation may happen ...
There are tasks when it is necessary on top of the existing base, to which a certain web editor is already tied. And the likelihood that the existing scheme will fully meet the requirements of the
ORM is small. As a consequence, when configuring models, it is necessary to pull the whole thing.

A fairly common practice, for normalization, is the use of reference tables.
For example, it may be a contact table associated with the contact type directory. In this case, it would be logical to make a check of the entered data at the model level, you can add methods for formatting values, and so on.
To solve this problem there are two ways:
- take advantage of the STI , it directly begs here;
- use one thick class in which the logic is defined through the case .
I don’t even consider the second option, since is too bulky and not too flexible. Therefore, we dwell on the first.
And so, to use the
STI, you need an additional field that will point to the class. To alter the scheme is possible, but the redundancy increases, which must be maintained in the correct state. In the case of the above example, when adding the
type field, the field value will have to be synchronized with the foreign key. Therefore, it would be logical to use the available data. Since determining the name of the class occurs before its creation, then you have to interfere with the work of
ActiveRecord itself.
Digging in the documentation and source code clarified the whole mechanism. The
instantiate method in the
ActiveRecord :: Inheritance module is responsible for it:
This method is quite simple:
- the class to be created is determined;
- if IdentityMap support is enabled, then we use it, otherwise we create a new instance based on data received from the database.
Consider how determined what class should be created. To do this, we look at the source code further, namely the
find_sti_class method
, to which the name of the type taken from the field of the corresponding
inheritance_column is transferred, by default, as already mentioned earlier, it is equal to
type.As you can see there is no special magic. Therefore, to solve the problem, it was necessary to override the instantiate method so that instead of the value from the field, another one derived from the related table is passed.
The resulting solution was designed as a Gem-a. Works on the same principle as the association.
ActiveRecord extends the additional method
acts_as_ati , which has the same syntax as the
belongs_to method.
@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
In this method, a hash with auxiliary information on communication is formed, the relation itself and validators are also added. In addition, the instance is expanded by a number of auxiliary methods + the overload is actually performed.
The overloaded method basically does not change, only the receipt of the class name based on the created relationship is added.
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))
This is where the major changes end. Using the resulting class, it turned out to implement the
STI through a related table. This approach has a minus in performance (in some places solved by data caching), but it also makes it possible to fully use polymorphism.
Usage example:
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| "
This solution is used in the work of the internal resource and so far it has shown itself only on the positive side and has made the code more readable and easy to maintain.
The gem is not yet hosted on rubygems, but it can be connected via a gemfile:
gem 'ext_sti', :git => 'git://github.com/fuCtor/ext_sti.git'
either as a local copy
gem 'ext_sti', :path => %path_to_ext_sti%