sequel-table_inheritance 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +21 -0
- data/README.rdoc +199 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/sequel/plugins/hybrid_table_inheritance.rb +480 -0
- data/sequel-table_inheritance.gemspec +26 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a970fd131bbbece81669fee96f7ce23277c59b65
|
4
|
+
data.tar.gz: 8c0db19ecf4ec040f34a00e71053e8b1c76fc5b3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c3b70ea2393962841410421938bef62f8dabdb5da6af50728334b0c8dbf5ca2e060827cadeef5057e9f40e7b1aee26528dc508c5f3f78c152d39efd7cbab69dc
|
7
|
+
data.tar.gz: 85e639a0ccb4f8f5c0e82c955aa9f860e9576e80ac13d4668f1777a53e5b0568cfdd9ea03f312a3460b008b67767ba286d570713a8c5088acb4075d0f1f6ad8c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Quinn Harris
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
== Sequel Hybrid Table Inheritance
|
2
|
+
|
3
|
+
This is a sequel plugin that combines the functionality of the single and class
|
4
|
+
table inheritance plugins. This plugin uses the single_table_inheritance plugin
|
5
|
+
and should work as a drop in replacement for the class_table_inheritance plugins.
|
6
|
+
This allows using new tables for subclasses only when need for additional columns
|
7
|
+
or possibly referential integrity to exclusively subclassed rows.
|
8
|
+
|
9
|
+
== Additional features over the class table inheritance plugin
|
10
|
+
For class table inheritance use this provides additional functionality beyond
|
11
|
+
the standard class table inheritance plugin including the following:
|
12
|
+
|
13
|
+
* Eager loading in addition to lazy loading of subclasses
|
14
|
+
* Use RETURNING * on insert if available avoiding a select query after new model saves
|
15
|
+
* Features found in the single table inheritance not in class table inheritance
|
16
|
+
Notably the key_map, key_chooser options and accepting a proc in addition to
|
17
|
+
a hash for model_map option
|
18
|
+
|
19
|
+
== Documentation
|
20
|
+
=== Overview
|
21
|
+
|
22
|
+
The hybrid_table_inheritance pluging allows model subclasses to be stored
|
23
|
+
in either the same table as the parent model or a different table with a key
|
24
|
+
referencing the parent table.
|
25
|
+
This combines the functionality of single and class (multiple) table inheritance
|
26
|
+
into one plugin. This plugin uses the single_table_inheritance plugin
|
27
|
+
and should work as a drop in replacement for the class_table_inheritance plugins.
|
28
|
+
This allows introducing new tables only when needed typically for additional
|
29
|
+
fields or possibly referential integrity to subclassed objects.
|
30
|
+
|
31
|
+
=== Detail
|
32
|
+
|
33
|
+
For example, with this hierarchy:
|
34
|
+
|
35
|
+
Employee
|
36
|
+
/ \
|
37
|
+
Staff Manager
|
38
|
+
| |
|
39
|
+
Cook Executive
|
40
|
+
|
|
41
|
+
CEO
|
42
|
+
|
43
|
+
the following database schema may be used (table - columns):
|
44
|
+
|
45
|
+
employees :: id, name, kind
|
46
|
+
staff :: id, manager_id
|
47
|
+
managers :: id, num_staff
|
48
|
+
executives :: id, num_managers
|
49
|
+
|
50
|
+
The hybrid_table_inheritance plugin assumes that the root table
|
51
|
+
(e.g. employees) has a primary key field (usually autoincrementing),
|
52
|
+
and all other tables have a foreign key of the same name that points
|
53
|
+
to the same key in their superclass's table. In this example,
|
54
|
+
the employees id column is a primary key and the id column in every
|
55
|
+
other table is a foreign key referencing the employees id.
|
56
|
+
|
57
|
+
In this example the employees table stores Staff model objects and the
|
58
|
+
executives table stores CEO model objects.
|
59
|
+
|
60
|
+
When using the class_table_inheritance plugin, subclasses use joined
|
61
|
+
datasets:
|
62
|
+
|
63
|
+
Employee.dataset.sql
|
64
|
+
# SELECT * FROM employees
|
65
|
+
|
66
|
+
Manager.dataset.sql
|
67
|
+
# SELECT employees.id, employees.name, employees.kind,
|
68
|
+
# managers.num_staff
|
69
|
+
# FROM employees
|
70
|
+
# JOIN managers ON (managers.id = employees.id)
|
71
|
+
|
72
|
+
CEO.dataset.sql
|
73
|
+
# SELECT employees.id, employees.name, employees.kind,
|
74
|
+
# managers.num_staff, executives.num_managers
|
75
|
+
# FROM employees
|
76
|
+
# JOIN managers ON (managers.id = employees.id)
|
77
|
+
# JOIN executives ON (executives.id = managers.id)
|
78
|
+
# WHERE (employees.kind IN ('CEO'))
|
79
|
+
|
80
|
+
This allows CEO.all to return instances with all attributes
|
81
|
+
loaded. The plugin overrides the deleting, inserting, and updating
|
82
|
+
in the model to work with multiple tables, by handling each table
|
83
|
+
individually.
|
84
|
+
|
85
|
+
=== Subclass loading
|
86
|
+
|
87
|
+
When model objects are retrieved for a superclass the result could be
|
88
|
+
subclass objects needing additional attributes from other tables.
|
89
|
+
This plugin can load those additional attributes immediately with eager
|
90
|
+
loading or when requested on the object with lazy loading.
|
91
|
+
|
92
|
+
With eager loading, the additional needed rows will be loaded with the
|
93
|
+
all or first methods. Note that eager loading does not work
|
94
|
+
with the each method because all of the records must be loaded to
|
95
|
+
determine the keys for each subclass query. In that case lazy loading can
|
96
|
+
be used or the each method used on the result of the all method.
|
97
|
+
|
98
|
+
If lazy loading is used the lazy_attributes plugin will be included to
|
99
|
+
return subclass specific attributes that were not loaded
|
100
|
+
when calling superclass methods (since those wouldn't join
|
101
|
+
to the subclass tables). For example:
|
102
|
+
|
103
|
+
a = Employee.all # [<#Staff>, <#Manager>, <#Executive>]
|
104
|
+
a.first.values # {:id=>1, name=>'S', :kind=>'Staff'}
|
105
|
+
a.first.manager_id # Loads the manager_id attribute from the database
|
106
|
+
|
107
|
+
If you want to get all columns in a subclass instance after loading
|
108
|
+
via the superclass, call Model#refresh.
|
109
|
+
|
110
|
+
a = Employee.first
|
111
|
+
a.values # {:id=>1, name=>'S', :kind=>'CEO'}
|
112
|
+
a.refresh.values # {:id=>1, name=>'S', :kind=>'Executive', :num_staff=>4, :num_managers=>2}
|
113
|
+
|
114
|
+
The option :subclass_load sets the default subclass loading strategy.
|
115
|
+
It accepts :eager, :eager_only, :lazy or :lazy_only with a default of :lazy
|
116
|
+
The _only options will only allow that strategy to be used.
|
117
|
+
In addition eager or lazy can be called on a dataset to override the default
|
118
|
+
strategy used assuming an _only option was not set.
|
119
|
+
|
120
|
+
=== Usage
|
121
|
+
|
122
|
+
# Use the default of storing the class name in the sti_key
|
123
|
+
# column (:kind in this case)
|
124
|
+
class Employee < Sequel::Model
|
125
|
+
plugin :hybrid_table_inheritance, :key=>:kind
|
126
|
+
end
|
127
|
+
|
128
|
+
# Have subclasses inherit from the appropriate class
|
129
|
+
class Staff < Employee; end # uses staff table
|
130
|
+
class Cook < Staff; end # cooks table doesn't exist so uses staff table
|
131
|
+
class Manager < Employee; end # uses managers table
|
132
|
+
class Executive < Manager; end # uses executives table
|
133
|
+
class CEO < Executive; end # ceos table doesn't exist so uses executives table
|
134
|
+
|
135
|
+
# Some examples of using these options:
|
136
|
+
|
137
|
+
# Specifying the tables with a :table_map hash
|
138
|
+
Employee.plugin :hybrid_table_inheritance,
|
139
|
+
:table_map=>{:Employee => :employees,
|
140
|
+
:Staff => :staff,
|
141
|
+
:Cook => :staff,
|
142
|
+
:Manager => :managers,
|
143
|
+
:Executive => :executives,
|
144
|
+
:CEO => :executives }
|
145
|
+
|
146
|
+
# Using integers to store the class type, with a :model_map hash
|
147
|
+
# and an sti_key of :type
|
148
|
+
Employee.plugin :hybrid_table_inheritance, :type,
|
149
|
+
:model_map=>{1=>:Staff, 2=>:Cook, 3=>:Manager, 4=>:Executive, 5=>:CEO}
|
150
|
+
|
151
|
+
# Using non-class name strings
|
152
|
+
Employee.plugin :hybrid_table_inheritance, :key=>:type,
|
153
|
+
:model_map=>{'staff'=>:Staff, 'cook staff'=>:Cook, 'supervisor'=>:Manager}
|
154
|
+
|
155
|
+
# By default the plugin sets the respective column value
|
156
|
+
# when a new instance is created.
|
157
|
+
Cook.create.type == 'cook staff'
|
158
|
+
Manager.create.type == 'supervisor'
|
159
|
+
|
160
|
+
# You can customize this behavior with the :key_chooser option.
|
161
|
+
# This is most useful when using a non-bijective mapping.
|
162
|
+
Employee.plugin :hybrid_table_inheritance, :key=>:type,
|
163
|
+
:model_map=>{'cook staff'=>:Cook, 'supervisor'=>:Manager},
|
164
|
+
:key_chooser=>proc{|instance| instance.model.sti_key_map[instance.model.to_s].first || 'stranger' }
|
165
|
+
|
166
|
+
# Using custom procs, with :model_map taking column values
|
167
|
+
# and yielding either a class, string, symbol, or nil,
|
168
|
+
# and :key_map taking a class object and returning the column
|
169
|
+
# value to use
|
170
|
+
Employee.plugin :single_table_inheritance, :key=>:type,
|
171
|
+
:model_map=>proc{|v| v.reverse},
|
172
|
+
:key_map=>proc{|klass| klass.name.reverse}
|
173
|
+
|
174
|
+
# You can use the same class for multiple values.
|
175
|
+
# This is mainly useful when the sti_key column contains multiple values
|
176
|
+
# which are different but do not require different code.
|
177
|
+
Employee.plugin :single_table_inheritance, :key=>:type,
|
178
|
+
:model_map=>{'staff' => "Staff",
|
179
|
+
'manager' => "Manager",
|
180
|
+
'overpayed staff' => "Staff",
|
181
|
+
'underpayed staff' => "Staff"}
|
182
|
+
|
183
|
+
One minor issue to note is that if you specify the <tt>:key_map</tt>
|
184
|
+
option as a hash, instead of having it inferred from the <tt>:model_map</tt>,
|
185
|
+
you should only use class name strings as keys, you should not use symbols
|
186
|
+
as keys.
|
187
|
+
|
188
|
+
== Options
|
189
|
+
:key :: column symbol that holds the key that identifies the class to use.
|
190
|
+
Necessary if you want to call model methods on a superclass
|
191
|
+
that return subclass instances
|
192
|
+
:model_map :: Hash or proc mapping the key column values to model class names.
|
193
|
+
:key_map :: Hash or proc mapping model class names to key column values.
|
194
|
+
Each value or return is an array of possible key column values.
|
195
|
+
:key_chooser :: proc returning key for the provided model instance
|
196
|
+
:table_map :: Hash with class name symbols keys mapping to table name symbol values
|
197
|
+
Overrides implicit table names
|
198
|
+
:subclass_load :: subclass loading strategy, defaults to :lazy
|
199
|
+
options: :eager, :eager_only, :lazy or :lazy_only
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "sequel/plugins/hybrid_table_inheritance"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,480 @@
|
|
1
|
+
module Sequel
|
2
|
+
module Plugins
|
3
|
+
# = Overview
|
4
|
+
#
|
5
|
+
# The hybrid_table_inheritance pluging allows model subclasses to be stored
|
6
|
+
# in either the same table as the parent model or a different table with a key
|
7
|
+
# referencing the parent table.
|
8
|
+
# This combines the functionality of single and class (multiple) table inheritance
|
9
|
+
# into one plugin. This plugin uses the single_table_inheritance plugin
|
10
|
+
# and should work as a drop in replacement for the class_table_inheritance plugins.
|
11
|
+
# This allows introducing new tables only when needed typically for additional
|
12
|
+
# fields or possibly referential integrity to subclassed objects.
|
13
|
+
#
|
14
|
+
# = Detail
|
15
|
+
#
|
16
|
+
# For example, with this hierarchy:
|
17
|
+
#
|
18
|
+
# Employee
|
19
|
+
# / \
|
20
|
+
# Staff Manager
|
21
|
+
# | |
|
22
|
+
# Cook Executive
|
23
|
+
# |
|
24
|
+
# CEO
|
25
|
+
#
|
26
|
+
# the following database schema may be used (table - columns):
|
27
|
+
#
|
28
|
+
# employees :: id, name, kind
|
29
|
+
# staff :: id, manager_id
|
30
|
+
# managers :: id, num_staff
|
31
|
+
# executives :: id, num_managers
|
32
|
+
#
|
33
|
+
# The hybrid_table_inheritance plugin assumes that the root table
|
34
|
+
# (e.g. employees) has a primary key field (usually autoincrementing),
|
35
|
+
# and all other tables have a foreign key of the same name that points
|
36
|
+
# to the same key in their superclass's table. In this example,
|
37
|
+
# the employees id column is a primary key and the id column in every
|
38
|
+
# other table is a foreign key referencing the employees id.
|
39
|
+
#
|
40
|
+
# In this example the employees table stores Staff model objects and the
|
41
|
+
# executives table stores CEO model objects.
|
42
|
+
#
|
43
|
+
# When using the class_table_inheritance plugin, subclasses use joined
|
44
|
+
# datasets:
|
45
|
+
#
|
46
|
+
# Employee.dataset.sql
|
47
|
+
# # SELECT * FROM employees
|
48
|
+
#
|
49
|
+
# Manager.dataset.sql
|
50
|
+
# # SELECT employees.id, employees.name, employees.kind,
|
51
|
+
# # managers.num_staff
|
52
|
+
# # FROM employees
|
53
|
+
# # JOIN managers ON (managers.id = employees.id)
|
54
|
+
#
|
55
|
+
# CEO.dataset.sql
|
56
|
+
# # SELECT employees.id, employees.name, employees.kind,
|
57
|
+
# # managers.num_staff, executives.num_managers
|
58
|
+
# # FROM employees
|
59
|
+
# # JOIN managers ON (managers.id = employees.id)
|
60
|
+
# # JOIN executives ON (executives.id = managers.id)
|
61
|
+
# # WHERE (employees.kind IN ('CEO'))
|
62
|
+
#
|
63
|
+
# This allows CEO.all to return instances with all attributes
|
64
|
+
# loaded. The plugin overrides the deleting, inserting, and updating
|
65
|
+
# in the model to work with multiple tables, by handling each table
|
66
|
+
# individually.
|
67
|
+
#
|
68
|
+
# = Subclass loading
|
69
|
+
#
|
70
|
+
# When model objects are retrieved for a superclass the result could be
|
71
|
+
# subclass objects needing additional attributes from other tables.
|
72
|
+
# This plugin can load those additional attributes immediately with eager
|
73
|
+
# loading or when requested on the object with lazy loading.
|
74
|
+
#
|
75
|
+
# With eager loading, the additional needed rows will be loaded with the
|
76
|
+
# all or first methods. Note that eager loading does not work
|
77
|
+
# with the each method because all of the records must be loaded to
|
78
|
+
# determine the keys for each subclass query. In that case lazy loading can
|
79
|
+
# be used or the each method used on the result of the all method.
|
80
|
+
#
|
81
|
+
# If lazy loading is used the lazy_attributes plugin will be included to
|
82
|
+
# return subclass specific attributes that were not loaded
|
83
|
+
# when calling superclass methods (since those wouldn't join
|
84
|
+
# to the subclass tables). For example:
|
85
|
+
#
|
86
|
+
# a = Employee.all # [<#Staff>, <#Manager>, <#Executive>]
|
87
|
+
# a.first.values # {:id=>1, name=>'S', :kind=>'Staff'}
|
88
|
+
# a.first.manager_id # Loads the manager_id attribute from the database
|
89
|
+
#
|
90
|
+
# If you want to get all columns in a subclass instance after loading
|
91
|
+
# via the superclass, call Model#refresh.
|
92
|
+
#
|
93
|
+
# a = Employee.first
|
94
|
+
# a.values # {:id=>1, name=>'S', :kind=>'CEO'}
|
95
|
+
# a.refresh.values # {:id=>1, name=>'S', :kind=>'Executive', :num_staff=>4, :num_managers=>2}
|
96
|
+
#
|
97
|
+
# The option :subclass_load sets the default subclass loading strategy.
|
98
|
+
# It accepts :eager, :eager_only, :lazy or :lazy_only with a default of :lazy
|
99
|
+
# The _only options will only allow that strategy to be used.
|
100
|
+
# In addition eager or lazy can be called on a dataset to override the default
|
101
|
+
# strategy used assuming an _only option was not set.
|
102
|
+
#
|
103
|
+
# = Usage
|
104
|
+
#
|
105
|
+
# # Use the default of storing the class name in the sti_key
|
106
|
+
# # column (:kind in this case)
|
107
|
+
# class Employee < Sequel::Model
|
108
|
+
# plugin :hybrid_table_inheritance, :key=>:kind
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# # Have subclasses inherit from the appropriate class
|
112
|
+
# class Staff < Employee; end # uses staff table
|
113
|
+
# class Cook < Staff; end # cooks table doesn't exist so uses staff table
|
114
|
+
# class Manager < Employee; end # uses managers table
|
115
|
+
# class Executive < Manager; end # uses executives table
|
116
|
+
# class CEO < Executive; end # ceos table doesn't exist so uses executives table
|
117
|
+
#
|
118
|
+
# # Some examples of using these options:
|
119
|
+
#
|
120
|
+
# # Specifying the tables with a :table_map hash
|
121
|
+
# Employee.plugin :hybrid_table_inheritance,
|
122
|
+
# :table_map=>{:Employee => :employees,
|
123
|
+
# :Staff => :staff,
|
124
|
+
# :Cook => :staff,
|
125
|
+
# :Manager => :managers,
|
126
|
+
# :Executive => :executives,
|
127
|
+
# :CEO => :executives }
|
128
|
+
#
|
129
|
+
# # Using integers to store the class type, with a :model_map hash
|
130
|
+
# # and an sti_key of :type
|
131
|
+
# Employee.plugin :hybrid_table_inheritance, :type,
|
132
|
+
# :model_map=>{1=>:Staff, 2=>:Cook, 3=>:Manager, 4=>:Executive, 5=>:CEO}
|
133
|
+
#
|
134
|
+
# # Using non-class name strings
|
135
|
+
# Employee.plugin :hybrid_table_inheritance, :key=>:type,
|
136
|
+
# :model_map=>{'staff'=>:Staff, 'cook staff'=>:Cook, 'supervisor'=>:Manager}
|
137
|
+
#
|
138
|
+
# # By default the plugin sets the respective column value
|
139
|
+
# # when a new instance is created.
|
140
|
+
# Cook.create.type == 'cook staff'
|
141
|
+
# Manager.create.type == 'supervisor'
|
142
|
+
#
|
143
|
+
# # You can customize this behavior with the :key_chooser option.
|
144
|
+
# # This is most useful when using a non-bijective mapping.
|
145
|
+
# Employee.plugin :hybrid_table_inheritance, :key=>:type,
|
146
|
+
# :model_map=>{'cook staff'=>:Cook, 'supervisor'=>:Manager},
|
147
|
+
# :key_chooser=>proc{|instance| instance.model.sti_key_map[instance.model.to_s].first || 'stranger' }
|
148
|
+
#
|
149
|
+
# # Using custom procs, with :model_map taking column values
|
150
|
+
# # and yielding either a class, string, symbol, or nil,
|
151
|
+
# # and :key_map taking a class object and returning the column
|
152
|
+
# # value to use
|
153
|
+
# Employee.plugin :single_table_inheritance, :key=>:type,
|
154
|
+
# :model_map=>proc{|v| v.reverse},
|
155
|
+
# :key_map=>proc{|klass| klass.name.reverse}
|
156
|
+
#
|
157
|
+
# # You can use the same class for multiple values.
|
158
|
+
# # This is mainly useful when the sti_key column contains multiple values
|
159
|
+
# # which are different but do not require different code.
|
160
|
+
# Employee.plugin :single_table_inheritance, :key=>:type,
|
161
|
+
# :model_map=>{'staff' => "Staff",
|
162
|
+
# 'manager' => "Manager",
|
163
|
+
# 'overpayed staff' => "Staff",
|
164
|
+
# 'underpayed staff' => "Staff"}
|
165
|
+
#
|
166
|
+
# One minor issue to note is that if you specify the <tt>:key_map</tt>
|
167
|
+
# option as a hash, instead of having it inferred from the <tt>:model_map</tt>,
|
168
|
+
# you should only use class name strings as keys, you should not use symbols
|
169
|
+
# as keys.
|
170
|
+
module HybridTableInheritance
|
171
|
+
# The class_table_inheritance plugin requires the lazy_attributes plugin
|
172
|
+
# to handle lazily-loaded attributes for subclass instances returned
|
173
|
+
# by superclass methods.
|
174
|
+
def self.apply(model, opts = OPTS)
|
175
|
+
model.plugin :single_table_inheritance, nil
|
176
|
+
model.plugin :lazy_attributes unless opts[:subclass_load] == :eager_only
|
177
|
+
end
|
178
|
+
|
179
|
+
# Setup the plugin using the following options:
|
180
|
+
# :key :: column symbol that holds the key that identifies the class to use.
|
181
|
+
# Necessary if you want to call model methods on a superclass
|
182
|
+
# that return subclass instances
|
183
|
+
# :model_map :: Hash or proc mapping the key column values to model class names.
|
184
|
+
# :key_map :: Hash or proc mapping model class names to key column values.
|
185
|
+
# Each value or return is an array of possible key column values.
|
186
|
+
# :key_chooser :: proc returning key for the provided model instance
|
187
|
+
# :table_map :: Hash with class name symbols keys mapping to table name symbol values
|
188
|
+
# Overrides implicit table names
|
189
|
+
# :subclass_load :: subclass loading strategy, defaults to :lazy
|
190
|
+
# options: :eager, :eager_only, :lazy or :lazy_only
|
191
|
+
def self.configure(model, opts = OPTS)
|
192
|
+
SingleTableInheritance.configure model, opts[:key], opts
|
193
|
+
|
194
|
+
model.instance_eval do
|
195
|
+
@cti_subclass_load = opts[:subclass_load]
|
196
|
+
@cti_subclass_datasets = {}
|
197
|
+
@cti_models = [self]
|
198
|
+
@cti_tables = [table_name]
|
199
|
+
@cti_instance_dataset = db.from(table_name)
|
200
|
+
@cti_table_columns = columns
|
201
|
+
@cti_table_map = opts[:table_map] || {}
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
module ClassMethods
|
206
|
+
# An array of each model in the inheritance hierarchy that uses an
|
207
|
+
# backed by a new table.
|
208
|
+
attr_reader :cti_models
|
209
|
+
|
210
|
+
# The parent/root/base model for this class table inheritance hierarchy.
|
211
|
+
# This is the only model in the hierarchy that loads the
|
212
|
+
# class_table_inheritance plugin.
|
213
|
+
# Only needed to be compatible with class_table_inheritance plugin
|
214
|
+
def cti_base_model
|
215
|
+
@cti_models.first
|
216
|
+
end
|
217
|
+
|
218
|
+
# Last model in the inheritance hierarchy to use a new table
|
219
|
+
def cti_table_model
|
220
|
+
@cti_models.last
|
221
|
+
end
|
222
|
+
|
223
|
+
# A hash with subclass models keys and datasets to load that subclass
|
224
|
+
# assuming the current model has already been loaded.
|
225
|
+
# Used for eager loading
|
226
|
+
attr_reader :cti_subclass_datasets
|
227
|
+
|
228
|
+
# Eager loading option
|
229
|
+
attr_reader :cti_subclass_load
|
230
|
+
|
231
|
+
# Hash with table name symbol keys and arrays of column symbol values,
|
232
|
+
# giving the columns to update in each backing database table.
|
233
|
+
# Only needed to be compatible with class_table_inheritance plugin
|
234
|
+
def cti_columns
|
235
|
+
h = {}
|
236
|
+
cti_models.each { |m| h[m.table_name] = m.cti_table_columns }
|
237
|
+
h
|
238
|
+
end
|
239
|
+
|
240
|
+
# An array of column symbols for the backing database table,
|
241
|
+
# giving the columns to update in each backing database table.
|
242
|
+
attr_reader :cti_table_columns
|
243
|
+
|
244
|
+
# The dataset that table instance datasets are based on.
|
245
|
+
# Used for database modifications
|
246
|
+
attr_reader :cti_instance_dataset
|
247
|
+
|
248
|
+
# An array of table symbols that back this model. The first is
|
249
|
+
# cti_base_model table symbol, and the last is the current model
|
250
|
+
# table symbol.
|
251
|
+
attr_reader :cti_tables
|
252
|
+
|
253
|
+
# A hash with class name symbol keys and table name symbol values.
|
254
|
+
# Specified with the :table_map option to the plugin, and used if
|
255
|
+
# the implicit naming is incorrect.
|
256
|
+
attr_reader :cti_table_map
|
257
|
+
|
258
|
+
def inherited(subclass)
|
259
|
+
ds = sti_dataset
|
260
|
+
|
261
|
+
# Prevent inherited in model/base.rb from setting the dataset
|
262
|
+
subclass.instance_eval { @dataset = nil }
|
263
|
+
|
264
|
+
@cti_tables.push ds.first_source_alias # Kludge to change filter to use root table
|
265
|
+
super # Call single_table_inheritance
|
266
|
+
@cti_tables.pop
|
267
|
+
|
268
|
+
cm = cti_models
|
269
|
+
ctm = cti_table_map
|
270
|
+
ct = cti_tables
|
271
|
+
ctc = cti_table_columns
|
272
|
+
cid = cti_instance_dataset
|
273
|
+
pk = primary_key
|
274
|
+
csl = cti_subclass_load
|
275
|
+
|
276
|
+
# Set table if this is a class table inheritance
|
277
|
+
table = nil
|
278
|
+
columns = nil
|
279
|
+
if (n = subclass.name) && !n.empty?
|
280
|
+
if table = ctm[n.to_sym]
|
281
|
+
columns = db.from(table).columns
|
282
|
+
else
|
283
|
+
table = subclass.implicit_table_name
|
284
|
+
columns = db.from(table).columns rescue nil
|
285
|
+
table = nil if !columns || columns.empty?
|
286
|
+
end
|
287
|
+
end
|
288
|
+
table = nil if table && (table == table_name)
|
289
|
+
|
290
|
+
subclass.instance_eval do
|
291
|
+
@cti_table_map = ctm
|
292
|
+
@cti_subclass_load = csl
|
293
|
+
@cti_subclass_datasets = {}
|
294
|
+
|
295
|
+
if table
|
296
|
+
if ct.length == 1
|
297
|
+
ds = ds.select(*self.columns.map{|cc| Sequel.qualify(table_name, Sequel.identifier(cc))})
|
298
|
+
end
|
299
|
+
sel_app = (columns - [pk]).map{|cc| Sequel.qualify(table, Sequel.identifier(cc))}
|
300
|
+
@sti_dataset = ds.join(table, pk=>pk).select_append(*sel_app)
|
301
|
+
set_dataset(@sti_dataset)
|
302
|
+
set_columns(self.columns)
|
303
|
+
dataset.row_proc = lambda{|r| subclass.sti_load(r)}
|
304
|
+
|
305
|
+
@cti_models = cm + [self]
|
306
|
+
@cti_tables = ct + [table]
|
307
|
+
@cti_table_columns = columns
|
308
|
+
@cti_instance_dataset = db.from(table_name)
|
309
|
+
|
310
|
+
unless csl == :lazy_only
|
311
|
+
cm.each do |model|
|
312
|
+
sd = model.instance_variable_get(:@cti_subclass_datasets)
|
313
|
+
unless d = sd[cm.last]
|
314
|
+
sd[self] = db.from(table).select(*columns.map{|cc| Sequel.qualify(table_name, Sequel.identifier(cc))})
|
315
|
+
else
|
316
|
+
sd[self] = d.join(table, pk=>pk).select_append(*sel_app)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
unless csl == :eager_only
|
321
|
+
(columns - [pk]).each{|a| define_lazy_attribute_getter(a, :dataset=>dataset, :table=>table)}
|
322
|
+
end
|
323
|
+
cti_tables.reverse.each do |ct|
|
324
|
+
db.schema(ct).each{|sk,v| db_schema[sk] = v}
|
325
|
+
end
|
326
|
+
else
|
327
|
+
@cti_models = cm
|
328
|
+
@cti_tables = ct
|
329
|
+
@cti_table_columns = ctc
|
330
|
+
@cti_instance_dataset = cid
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# The table name for the current model class's main table.
|
336
|
+
def table_name
|
337
|
+
cti_tables ? cti_tables.last : super
|
338
|
+
end
|
339
|
+
|
340
|
+
def sti_class_from_key(key)
|
341
|
+
sti_class(sti_model_map[key])
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
module InstanceMethods
|
346
|
+
# Delete the row from all backing tables, starting from the
|
347
|
+
# most recent table and going through all superclasses.
|
348
|
+
def delete
|
349
|
+
raise Sequel::Error, "can't delete frozen object" if frozen?
|
350
|
+
model.cti_models.reverse.each do |m|
|
351
|
+
cti_this(m).delete
|
352
|
+
end
|
353
|
+
self
|
354
|
+
end
|
355
|
+
|
356
|
+
private
|
357
|
+
|
358
|
+
def cti_this(model)
|
359
|
+
use_server(model.cti_instance_dataset.filter(model.primary_key_hash(pk)))
|
360
|
+
end
|
361
|
+
|
362
|
+
# Set the sti_key column based on the sti_key_map.
|
363
|
+
def _before_validation
|
364
|
+
if new? && (set = self[model.sti_key])
|
365
|
+
exp = model.sti_key_chooser.call(self)
|
366
|
+
if set != exp
|
367
|
+
set_table = model.sti_class_from_key(set).table_name
|
368
|
+
exp_table = model.sti_class_from_key(exp).table_name
|
369
|
+
set_column_value("#{model.sti_key}=", exp) if set_table != exp_table
|
370
|
+
end
|
371
|
+
end
|
372
|
+
super
|
373
|
+
end
|
374
|
+
|
375
|
+
# Insert rows into all backing tables, using the columns
|
376
|
+
# in each table.
|
377
|
+
def _insert
|
378
|
+
return super if model.cti_tables.length == 1
|
379
|
+
model.cti_models.each do |m|
|
380
|
+
v = {}
|
381
|
+
m.cti_table_columns.each{|c| v[c] = @values[c] if @values.include?(c)}
|
382
|
+
ds = use_server(m.cti_instance_dataset)
|
383
|
+
if ds.supports_insert_select? && (h = ds.insert_select(v))
|
384
|
+
@values.merge!(h)
|
385
|
+
else
|
386
|
+
nid = ds.insert(v)
|
387
|
+
@values[primary_key] ||= nid
|
388
|
+
end
|
389
|
+
end
|
390
|
+
db.dataset.supports_insert_select? ? nil : @values[primary_key]
|
391
|
+
end
|
392
|
+
|
393
|
+
# Update rows in all backing tables, using the columns in each table.
|
394
|
+
def _update(columns)
|
395
|
+
model.cti_models.each do |m|
|
396
|
+
h = {}
|
397
|
+
m.cti_table_columns.each{|c| h[c] = columns[c] if columns.include?(c)}
|
398
|
+
cti_this(m).update(h) unless h.empty?
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
module DatasetMethods
|
404
|
+
def single_record
|
405
|
+
post_load_record(super)
|
406
|
+
end
|
407
|
+
|
408
|
+
def with_sql_first(sql)
|
409
|
+
post_load_record(super)
|
410
|
+
end
|
411
|
+
|
412
|
+
# Set dataset to use eager loading
|
413
|
+
def eager
|
414
|
+
raise Error, "eager loading disabled" if model.cti_subclass_load == :lazy_only
|
415
|
+
clone(:eager_load => true)
|
416
|
+
end
|
417
|
+
|
418
|
+
# Set dataset to use lazy loading
|
419
|
+
def lazy
|
420
|
+
raise Error, "lazy loading disabled" if model.cti_subclass_load == :eager_only
|
421
|
+
clone(:eager_load => false)
|
422
|
+
end
|
423
|
+
|
424
|
+
# Return true if eager loading will be used, false/nil for lazy loading
|
425
|
+
def uses_eager_load?
|
426
|
+
return opts[:eager_load] unless opts[:eager_load].nil?
|
427
|
+
[:eager, :eager_only].include?(model.cti_subclass_load)
|
428
|
+
end
|
429
|
+
|
430
|
+
private
|
431
|
+
|
432
|
+
def subclass_dataset(m, keys)
|
433
|
+
ds = model.cti_subclass_datasets[m]
|
434
|
+
ds = ds.filter(m.qualified_primary_key_hash(keys, ds.first_source_alias))
|
435
|
+
ds
|
436
|
+
end
|
437
|
+
|
438
|
+
def post_load_record(r)
|
439
|
+
return r unless r && uses_eager_load?
|
440
|
+
ds = subclass_dataset(r.model.cti_table_model, r.pk)
|
441
|
+
r.values.merge!(ds.limit(1).first)
|
442
|
+
r
|
443
|
+
end
|
444
|
+
|
445
|
+
def post_load(records)
|
446
|
+
super
|
447
|
+
return unless uses_eager_load?
|
448
|
+
|
449
|
+
subclass_datasets = model.cti_subclass_datasets
|
450
|
+
|
451
|
+
table_key_map = Hash.new
|
452
|
+
records.each do |r|
|
453
|
+
model = r.model.cti_table_model
|
454
|
+
next unless subclass_datasets.key?(model)
|
455
|
+
table_key_map[model] = { } unless table_key_map.key?(model)
|
456
|
+
table_key_map[model][r.pk] = r
|
457
|
+
end
|
458
|
+
|
459
|
+
pkc = model.primary_key
|
460
|
+
table_key_map.each do |model, id_map|
|
461
|
+
ds = subclass_dataset(model, id_map.keys)
|
462
|
+
applied_set = {}
|
463
|
+
ds.all do |r|
|
464
|
+
pkv = pkc.is_a?(Array) ? pkc.map{|k| r[k]} : r[pkc]
|
465
|
+
m = id_map[pkv]
|
466
|
+
if applied_set.key?(pkv)
|
467
|
+
# Multiple rows for one row in original query
|
468
|
+
# insert is O(n) but needed if dataset is ordered.
|
469
|
+
# This code path should seldom if ever get used
|
470
|
+
records.insert(records.index(m)+1, m = m.dup)
|
471
|
+
end
|
472
|
+
applied_set[pkv] = true
|
473
|
+
m.values.merge!(r)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
#require 'sequel/table_inheritance/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "sequel-table_inheritance"
|
8
|
+
s.version = '0.1.0' #Sequel::TableInheritance::VERSION
|
9
|
+
s.authors = ["Quinn Harris"]
|
10
|
+
s.email = ["sequel@quinnharris.me"]
|
11
|
+
|
12
|
+
s.summary = "Alternative to single and class table inheritance plugins for sequel"
|
13
|
+
s.description = s.summary
|
14
|
+
s.homepage = "https://github.com/QuinnHarris/sequel-table_inheritance"
|
15
|
+
s.license = "MIT"
|
16
|
+
|
17
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
s.bindir = "bin"
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "bundler", "~> 1.9"
|
22
|
+
s.add_development_dependency "rake", "~> 10.0"
|
23
|
+
s.add_development_dependency "rspec"
|
24
|
+
|
25
|
+
s.add_dependency "sequel", "~> 4.19"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sequel-table_inheritance
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Quinn Harris
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sequel
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.19'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.19'
|
69
|
+
description: Alternative to single and class table inheritance plugins for sequel
|
70
|
+
email:
|
71
|
+
- sequel@quinnharris.me
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- Gemfile
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.rdoc
|
80
|
+
- Rakefile
|
81
|
+
- bin/console
|
82
|
+
- bin/setup
|
83
|
+
- lib/sequel/plugins/hybrid_table_inheritance.rb
|
84
|
+
- sequel-table_inheritance.gemspec
|
85
|
+
homepage: https://github.com/QuinnHarris/sequel-table_inheritance
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata: {}
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.4.5
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: Alternative to single and class table inheritance plugins for sequel
|
109
|
+
test_files: []
|