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) # Employee.plugin :single_table_inheritance, :kind # # # 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} # # # 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{|v| v.reverse}, # :key_map=>proc{|klass| klass.name.reverse} # # 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={}) 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{|h,k| h[k.to_s] unless k.is_a?(String)} h.merge!(km) else km end elsif sti_model_map.is_a?(Hash) h = Hash.new{|h,k| h[k.to_s] unless k.is_a?(String)} sti_model_map.each do |k,v| h[v.to_s] = k end h else lambda{|klass| klass.name.to_s} end 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 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 # 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 sk = sti_key sd = sti_dataset skm = sti_key_map smm = sti_model_map key = skm[subclass] sti_subclass_added(key) ska = [key] rp = dataset.row_proc subclass.set_dataset(sd.filter(SQL::QualifiedIdentifier.new(table_name, sk)=>ska), :inherited=>true) subclass.instance_eval do dataset.row_proc = rp @sti_key = sk @sti_key_array = ska @sti_dataset = sd @sti_key_map = skm @sti_model_map = smm @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]]).load(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 sti_key_array << key superclass.sti_subclass_added(key) end end private # 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 # Set the sti_key column based on the sti_key_map. def before_create send("#{model.sti_key}=", model.sti_key_map[model]) unless self[model.sti_key] super end end end end end