Module: Sequel::Plugins::ClassTableInheritance
- Defined in:
- lib/sequel/plugins/class_table_inheritance.rb
Overview
Overview
The class_table_inheritance plugin uses the single_table_inheritance plugin, so it supports all of the single_table_inheritance features, but it additionally supports subclasses that have additional columns, which are stored in a separate table with a key referencing the primary table.
Detail
For example, with this hierarchy:
Employee
/ \
Staff Manager
| |
Cook Executive
|
CEO
the following database schema may be used (table - columns):
- employees
-
id, name, kind
- staff
-
id, manager_id
- managers
-
id, num_staff
- executives
-
id, num_managers
The class_table_inheritance plugin assumes that the root table (e.g. employees) has a primary key column (usually autoincrementing), and all other tables have a foreign key of the same name that points to the same column in their superclass’s table, which is also the primary key for that table. In this example, the employees table has an id column is a primary key and the id column in every other table is a foreign key referencing employees.id, which is also the primary key of that table.
Additionally, note that other than the primary key column, no subclass table has a column with the same name as any superclass table. This plugin does not support cases where the column names in a subclass table overlap with any column names in a superclass table.
In this example the staff table also stores Cook model objects and the executives table also stores CEO model objects.
When using the class_table_inheritance plugin, subclasses that have additional columns use joined datasets in subselects:
Employee.dataset.sql
# SELECT * FROM employees
Manager.dataset.sql
# SELECT * FROM (
# SELECT employees.id, employees.name, employees.kind,
# managers.num_staff
# FROM employees
# JOIN managers ON (managers.id = employees.id)
# ) AS employees
CEO.dataset.sql
# SELECT * FROM (
# SELECT employees.id, employees.name, employees.kind,
# managers.num_staff, executives.num_managers
# FROM employees
# JOIN managers ON (managers.id = employees.id)
# JOIN executives ON (executives.id = managers.id)
# WHERE (employees.kind IN ('CEO'))
# ) AS employees
This allows CEO.all to return instances with all attributes loaded. The plugin overrides the deleting, inserting, and updating in the model to work with multiple tables, by handling each table individually.
Subclass loading
When model objects are retrieved for a superclass the result can contain subclass instances that only have column entries for the columns in the superclass table. Calling the column method on the subclass instance for a column not in the superclass table will cause a query to the database to get the value for that column. If the subclass instance was retreived using Dataset#all, the query to the database will attempt to load the column values for all subclass instances that were retrieved. For example:
a = Employee.all # [<#Staff>, <#Manager>, <#Executive>]
a.first.values # {:id=>1, name=>'S', :kind=>'Staff'}
a.first.manager_id # Loads the manager_id attribute from the database
If you want to get all columns in a subclass instance after loading via the superclass, call Model#refresh.
a = Employee.first
a.values # {:id=>1, name=>'S', :kind=>'CEO'}
a.refresh.values # {:id=>1, name=>'S', :kind=>'CEO', :num_staff=>4, :num_managers=>2}
You can also load directly from a subclass:
a = Executive.first
a.values # {:id=>1, name=>'S', :kind=>'Executive', :num_staff=>4, :num_managers=>2}
Note that when loading from a subclass, because the subclass dataset uses a subquery that by default uses the same alias at the primary table, any qualified identifiers should reference the subquery alias (and qualified identifiers should not be needed unless joining to another table):
a = Executive.where(id: 1).first # works
a = Executive.where{{employees[:id]=>1}}.first # works
a = Executive.where{{executives[:id]=>1}}.first # doesn't work
Note that because subclass datasets select from a subquery, you cannot update, delete, or insert into them directly. To delete related rows, you need to go through the related tables and remove the related rows. Code that does this would be similar to:
pks = Executive.where{num_staff < 10}.select_map(:id)
Executive.cti_tables.reverse_each do |table|
DB.from(table).where(id: pks).delete
end
Usage
# Use the default of storing the class name in the sti_key
# column (:kind in this case)
class Employee < Sequel::Model
plugin :class_table_inheritance, key: :kind
end
# Have subclasses inherit from the appropriate class
class Staff < Employee; end # uses staff table
class Cook < Staff; end # cooks table doesn't exist so uses staff table
class Manager < Employee; end # uses managers table
class Executive < Manager; end # uses executives table
class CEO < Executive; end # ceos table doesn't exist so uses executives table
# Some examples of using these options:
# Specifying the tables with a :table_map hash
Employee.plugin :class_table_inheritance,
table_map: {Employee: :employees,
Staff: :staff,
Cook: :staff,
Manager: :managers,
Executive: :executives,
CEO: :executives }
# Using integers to store the class type, with a :model_map hash
# and an sti_key of :type
Employee.plugin :class_table_inheritance, key: :type,
model_map: {1=>:Staff, 2=>:Cook, 3=>:Manager, 4=>:Executive, 5=>:CEO}
# Using non-class name strings
Employee.plugin :class_table_inheritance, key: :type,
model_map: {'staff'=>:Staff, 'cook staff'=>:Cook, 'supervisor'=>:Manager}
# By default the plugin sets the respective column value
# when a new instance is created.
Cook.create.type == 'cook 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 :class_table_inheritance, key: :type,
model_map: {'cook staff'=>:Cook, '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, key: :type,
model_map: proc{|v| v.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, key: :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.
Defined Under Namespace
Modules: ClassMethods, InstanceMethods
Class Method Summary collapse
-
.apply(model, opts = OPTS) ⇒ Object
The class_table_inheritance plugin requires the single_table_inheritance plugin and the lazy_attributes plugin to handle lazily-loaded attributes for subclass instances returned by superclass methods.
-
.configure(model, opts = OPTS) ⇒ Object
- Initialize the plugin using the following options: :alias
-
Change the alias used for the subquery in model datasets.
Class Method Details
.apply(model, opts = OPTS) ⇒ Object
The class_table_inheritance plugin requires the single_table_inheritance plugin and the lazy_attributes plugin to handle lazily-loaded attributes for subclass instances returned by superclass methods.
192 193 194 195 |
# File 'lib/sequel/plugins/class_table_inheritance.rb', line 192 def self.apply(model, opts = OPTS) model.plugin :single_table_inheritance, nil model.plugin :lazy_attributes end |
.configure(model, opts = OPTS) ⇒ Object
Initialize the plugin using the following options:
- :alias
-
Change the alias used for the subquery in model datasets. using this as the alias.
- :key
-
Column symbol that holds the key that identifies the class to use. Necessary if you want to call model methods on a superclass that return subclass instances
- :model_map
-
Hash or proc mapping the key column values to model class names.
- :key_map
-
Hash or proc mapping model class names to key column values. Each value or return is an array of possible key column values.
- :key_chooser
-
proc returning key for the provided model instance
- :table_map
-
Hash with class name symbols keys mapping to table name symbol values. Overrides implicit table names.
- :ignore_subclass_columns
-
Array with column names as symbols that are ignored on all sub-classes.
- :qualify_tables
-
Boolean true to qualify automatically determined subclass tables with the same qualifier as their superclass.
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/sequel/plugins/class_table_inheritance.rb', line 214 def self.configure(model, opts = OPTS) SingleTableInheritance.configure model, opts[:key], opts model.instance_exec do @cti_models = [self] @cti_tables = [table_name] @cti_instance_dataset = @instance_dataset @cti_table_columns = columns @cti_table_map = opts[:table_map] || {} @cti_alias = opts[:alias] || case source = @dataset.first_source when SQL::QualifiedIdentifier @dataset.unqualified_column_for(source) else source end @cti_ignore_subclass_columns = opts[:ignore_subclass_columns] || [] @cti_qualify_tables = !!opts[:qualify_tables] end end |