module Sequel::Plugins::SingleTableInheritance

  1. lib/sequel/plugins/single_table_inheritance.rb

The single_table_inheritance plugin allows storing all objects in the same class hierarchy in the same table. It makes it so subclasses of this model only load rows related to the subclass, and when you retrieve rows from the main class, you get instances of the subclasses (if the rows should use the subclasses’s class).

By default, the plugin assumes that the sti_key column (the first argument to the plugin) holds the class name as a string. However, you can override this by using the :model_map option and/or the :key_map option.

The sti_key column can be set via mass assignment, just like any non-primary key column. Make sure to follow the recommendations in the mass assignment guide to ensure this column is not set with untrusted data.

You should only load this plugin in the parent class, not in the subclasses.

You shouldn’t call set_dataset in the model after applying this plugin, otherwise subclasses might use the wrong dataset. You should make sure this plugin is loaded before the subclasses. Note that since you need to load the plugin before the subclasses are created, you can’t use direct class references in the plugin class. You should specify subclasses in the plugin call using class name strings or symbols, see usage below.

Usage:

# Use the default of storing the class name in the sti_key
# column (:kind in this case)
class Employee < Sequel::Model
  plugin :single_table_inheritance, :kind
end

# Have subclasses inherit from the appropriate class
class Staff < Employee; end
class Manager < Employee; end

# You can also use many different options to configure the plugin:

# Using integers to store the class type, with a :model_map hash
# and an sti_key of :type
Employee.plugin :single_table_inheritance, :type,
  model_map: {1=>:Staff, 2=>:Manager}

# Using non-class name strings
Employee.plugin :single_table_inheritance, :type,
  model_map: {'line staff'=>:Staff, 'supervisor'=>:Manager}

# By default the plugin sets the respective column value
# when a new instance is created.
Staff.create.type == 'line staff'
Manager.create.type == 'supervisor'

# You can customize this behavior with the :key_chooser option.
# This is most useful when using a non-bijective mapping.
Employee.plugin :single_table_inheritance, :type,
  model_map: {'line staff'=>:Staff, 'supervisor'=>:Manager},
  key_chooser: lambda{|instance| instance.model.sti_key_map[instance.model.to_s].first || 'stranger'}

# Using custom procs, with :model_map taking column values
# and yielding either a class, string, symbol, or nil,
# and :key_map taking a class object and returning the column
# value to use
Employee.plugin :single_table_inheritance, :type,
  model_map: :reverse.to_proc,
  key_map: lambda{|klass| klass.name.reverse}

# You can use the same class for multiple values.
# This is mainly useful when the sti_key column contains multiple values
# which are different but do not require different code.
Employee.plugin :single_table_inheritance, :type,
  model_map: {'staff' => "Staff",
               'manager' => "Manager",
               'overpayed staff' => "Staff",
               'underpayed staff' => "Staff"}

One minor issue to note is that if you specify the :key_map option as a hash, instead of having it inferred from the :model_map, you should only use class name strings as keys, you should not use symbols as keys.

Methods

Public Class

  1. configure

Public Class methods

configure(model, key, opts=OPTS)

Setup the necessary STI variables, see the module RDoc for SingleTableInheritance

[show source]
    # File lib/sequel/plugins/single_table_inheritance.rb
 87 def self.configure(model, key, opts=OPTS)
 88   model.instance_exec do
 89     @sti_key_array = nil
 90     @sti_key = key 
 91     @sti_dataset = dataset
 92     @sti_root_class = self
 93     @sti_model_map = opts[:model_map] || lambda{|v| v if v && v != ''}
 94     @sti_key_map = if km = opts[:key_map]
 95       if km.is_a?(Hash)
 96         h = Hash.new do |h1,k| 
 97           unless k.is_a?(String)
 98             h1[k.to_s]
 99           else
100             []
101           end
102         end
103         km.each do |k,v|
104           h[k.to_s] = [] unless h.key?(k.to_s)
105           h[k.to_s].push( *Array(v) )
106         end
107         h
108       else
109         km
110       end
111     elsif sti_model_map.is_a?(Hash)
112       h = Hash.new do |h1,k| 
113         unless k.is_a?(String)
114           h1[k.to_s]
115         else
116           []
117         end
118       end
119       sti_model_map.each do |k,v|
120         h[v.to_s] = [] unless h.key?(v.to_s)
121         h[v.to_s] << k
122       end
123       h
124     else
125       lambda{|klass| klass.name.to_s}
126     end
127     @sti_key_chooser = opts[:key_chooser] || lambda{|inst| Array(inst.model.sti_key_map[inst.model]).last }
128 
129     @dataset = @dataset.with_row_proc(model.method(:sti_load))
130   end
131 end