module Sequel module Plugins # 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. # # 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=>proc{|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=>proc(&:reverse), # :key_map=>proc{|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. module SingleTableInheritance # Setup the necessary STI variables, see the module RDoc for SingleTableInheritance def self.configure(model, key, opts=OPTS) model.instance_eval do @sti_key_array = nil @sti_key = key @sti_dataset = dataset @sti_model_map = opts[:model_map] || lambda{|v| v if v && v != ''} @sti_key_map = if km = opts[:key_map] if km.is_a?(Hash) h = Hash.new do |h1,k| unless k.is_a?(String) h1[k.to_s] else [] end end km.each do |k,v| h[k.to_s] = [ ] unless h.key?(k.to_s) h[k.to_s].push( *Array(v) ) end h else km end elsif sti_model_map.is_a?(Hash) h = Hash.new do |h1,k| unless k.is_a?(String) h1[k.to_s] else [] end end sti_model_map.each do |k,v| h[v.to_s] = [ ] unless h.key?(v.to_s) h[v.to_s] << k end h else lambda{|klass| klass.name.to_s} end @sti_key_chooser = opts[:key_chooser] || lambda{|inst| Array(inst.model.sti_key_map[inst.model]).last } dataset.row_proc = lambda{|r| model.sti_load(r)} end end module ClassMethods # The base dataset for STI, to which filters are added to get # only the models for the specific STI subclass. attr_reader :sti_dataset # The column name holding the STI key for this model attr_reader :sti_key # Array holding keys for all subclasses of this class, used for the # dataset filter in subclasses. Nil in the main class. attr_reader :sti_key_array # A hash/proc with class keys and column value values, mapping # the class to a particular value given to the sti_key column. # Used to set the column value when creating objects, and for the # filter when retrieving objects in subclasses. attr_reader :sti_key_map # A hash/proc with column value keys and class values, mapping # the value of the sti_key column to the appropriate class to use. attr_reader :sti_model_map # A proc which returns the value to use for new instances. # This defaults to a lookup in the key map. attr_reader :sti_key_chooser Plugins.inherited_instance_variables(self, :@sti_dataset=>nil, :@sti_key=>nil, :@sti_key_map=>nil, :@sti_model_map=>nil, :@sti_key_chooser=>nil) # Copy the necessary attributes to the subclasses, and filter the # subclass's dataset based on the sti_kep_map entry for the class. def inherited(subclass) super key = Array(sti_key_map[subclass]).dup sti_subclass_added(key) rp = dataset.row_proc subclass.set_dataset(sti_dataset.filter(SQL::QualifiedIdentifier.new(sti_dataset.first_source_alias, sti_key)=>key), :inherited=>true) subclass.instance_eval do dataset.row_proc = rp @sti_key_array = key self.simple_table = nil end end # Return an instance of the class specified by sti_key, # used by the row_proc. def sti_load(r) sti_class(sti_model_map[r[sti_key]]).call(r) end # Make sure that all subclasses of the parent class correctly include # keys for all of their descendant classes. def sti_subclass_added(key) if sti_key_array key_array = Array(key) Sequel.synchronize{sti_key_array.push(*key_array)} superclass.sti_subclass_added(key) end end private # If calling set_dataset manually, make sure to set the dataset # row proc to one that handles inheritance correctly. def set_dataset_row_proc(ds) ds.row_proc = @dataset.row_proc if @dataset end # Return a class object. If a class is given, return it directly. # Treat strings and symbols as class names. If nil is given or # an invalid class name string or symbol is used, return self. # Raise an error for other types. def sti_class(v) case v when String, Symbol constantize(v) rescue self when nil self when Class v else raise(Error, "Invalid class type used: #{v.inspect}") end end end module InstanceMethods private # Set the sti_key column based on the sti_key_map. def _before_validation if new? && !self[model.sti_key] set_column_value("#{model.sti_key}=", model.sti_key_chooser.call(self)) end super end end end end end