multiple_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.
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/README.md +215 -0
- data/Rakefile +47 -0
- data/lib/multiple_table_inheritance/active_record/child.rb +125 -0
- data/lib/multiple_table_inheritance/active_record/child.rb.bak +132 -0
- data/lib/multiple_table_inheritance/active_record/migration.rb +20 -0
- data/lib/multiple_table_inheritance/active_record/parent.rb +65 -0
- data/lib/multiple_table_inheritance/active_record.rb +9 -0
- data/lib/multiple_table_inheritance/railtie.rb +22 -0
- data/lib/multiple_table_inheritance/version.rb +3 -0
- data/lib/multiple_table_inheritance.rb +7 -0
- data/multiiple_table_inheritance.gemspec +33 -0
- data/spec/active_record/child_spec.rb +58 -0
- data/spec/active_record/parent_spec.rb +7 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/support/classes.rb +40 -0
- data/spec/support/tables.rb +42 -0
- metadata +136 -0
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
Multiple Table Inheritance
|
2
|
+
==========================
|
3
|
+
|
4
|
+
Multiple Table Inheritance is a plugin designed to allow for multiple table
|
5
|
+
inheritance between your database tables and your ActiveRecord models.
|
6
|
+
|
7
|
+
This plugin is a derivative of the original class-table-inheritance gem, which
|
8
|
+
can be found at http://github.com/brunofrank/class-table-inheritance
|
9
|
+
|
10
|
+
|
11
|
+
Compatibility
|
12
|
+
=============
|
13
|
+
|
14
|
+
Multiple Table Inheritance is Rails 3.x compatible.
|
15
|
+
|
16
|
+
|
17
|
+
How to Install
|
18
|
+
==============
|
19
|
+
|
20
|
+
From the command line:
|
21
|
+
|
22
|
+
gem install multiple-table-inheritance
|
23
|
+
|
24
|
+
From your Gemfile:
|
25
|
+
|
26
|
+
gem 'multiple_table_inheritance', '~> 0.1.0'
|
27
|
+
|
28
|
+
Usage
|
29
|
+
=====
|
30
|
+
|
31
|
+
Running Migrations
|
32
|
+
------------------
|
33
|
+
|
34
|
+
When creating your tables, the table representing the superclass must include a
|
35
|
+
`subtype` string column. (Optionally, you can provide a custom name for this
|
36
|
+
field and provide the custom name in your model options.) It is recommended
|
37
|
+
that this column be non-null for sanity.
|
38
|
+
|
39
|
+
create_table :employees do |t|
|
40
|
+
t.string :subtype, :null => false
|
41
|
+
t.string :first_name, :null => false
|
42
|
+
t.string :last_name, :null => false
|
43
|
+
t.integer :salary, :null => false
|
44
|
+
t.timestamps
|
45
|
+
end
|
46
|
+
|
47
|
+
When creating tables that are derived from your superclass table, simple
|
48
|
+
provide the `:inherits` hash option to your `create_table` call.
|
49
|
+
|
50
|
+
create_table :programmers, :inherits => :employee do |t|
|
51
|
+
t.datetime :training_completed_at
|
52
|
+
end
|
53
|
+
|
54
|
+
create_table :managers, :inherits => :employee do |t|
|
55
|
+
t.integer :bonus, :null => false
|
56
|
+
end
|
57
|
+
|
58
|
+
Creating Models
|
59
|
+
---------------
|
60
|
+
|
61
|
+
The `acts_as_superclass` method is used to represent that a model can be
|
62
|
+
extended.
|
63
|
+
|
64
|
+
class Employee < ActiveRecord::Base
|
65
|
+
acts_as_superclass
|
66
|
+
end
|
67
|
+
|
68
|
+
Conversely, the `inherits_from` method is used to represent that a given model
|
69
|
+
extends another model. It takes one optional parameter, which is the symbol
|
70
|
+
desired for referencing the relationship.
|
71
|
+
|
72
|
+
class Programmer < ActiveRecord::Base
|
73
|
+
inherits_from :employee
|
74
|
+
end
|
75
|
+
|
76
|
+
class Manager < ActiveRecord::Base
|
77
|
+
inherits_from :employee
|
78
|
+
end
|
79
|
+
|
80
|
+
Additional options can be passed to represent the exact relationship structure.
|
81
|
+
Specifically, any option that can be provided to `belongs_to` can be provided
|
82
|
+
to `inherits_from`. (Presently, this has only be tested to work with the
|
83
|
+
`:class_name` option.)
|
84
|
+
|
85
|
+
class Resources::Manager < ActiveRecord::Base
|
86
|
+
inherits_from :employee, :class_name => 'Resources::Employee'
|
87
|
+
end
|
88
|
+
|
89
|
+
Creating Records
|
90
|
+
----------------
|
91
|
+
|
92
|
+
Programmer.create(
|
93
|
+
:first_name => 'Bob',
|
94
|
+
:last_name => 'Smith',
|
95
|
+
:salary => 65000,
|
96
|
+
:training_completed_at => 3.years.ago)
|
97
|
+
|
98
|
+
Manager.create(
|
99
|
+
:first_name => 'Joe'
|
100
|
+
:last_name => 'Schmoe',
|
101
|
+
:salary => 75000,
|
102
|
+
:bonus => 5000)
|
103
|
+
|
104
|
+
Retrieving Records
|
105
|
+
------------------
|
106
|
+
|
107
|
+
Records can be retrieved explicitly by their own type.
|
108
|
+
|
109
|
+
programmer = Programmer.first # <Programmer employee_id: 1 training_completed_at: "2009-03-06 00:30:00">
|
110
|
+
programmer.id # 1
|
111
|
+
programmer.first_name # "Bob"
|
112
|
+
programmer.last_name # "Smith"
|
113
|
+
programmer.salary # 65000
|
114
|
+
|
115
|
+
Records can be retrieved implicitly by the superclass type.
|
116
|
+
|
117
|
+
employees = Employee.limit(2) # [<Programmer employee_id: 1 training_completed_at: "2009-03-06 00:30:00">,
|
118
|
+
<Manager employee_id: 2 bonus: 5000>]
|
119
|
+
employees.first.class # Programmer
|
120
|
+
employees.last.class # Manager
|
121
|
+
employees.first.bonus # undefined method `bonus`
|
122
|
+
employees.last.bonus # 5000
|
123
|
+
|
124
|
+
Deleting Records
|
125
|
+
----------------
|
126
|
+
|
127
|
+
Records can be deleted by either the parent or child reference.
|
128
|
+
|
129
|
+
Manager.first.destroy # destroys associated Employee reference as well
|
130
|
+
Employee.first.destroy # destroys associated Manager reference as well
|
131
|
+
|
132
|
+
Validation
|
133
|
+
----------
|
134
|
+
|
135
|
+
When creating a new record that inherits from another model, validation is
|
136
|
+
taken into consideration across both models.
|
137
|
+
|
138
|
+
class ::Employee < ActiveRecord::Base
|
139
|
+
acts_as_superclass
|
140
|
+
validates :first_name, :presence => true
|
141
|
+
validates :last_name, :presence => true
|
142
|
+
validates :salary, :presence => true, :numericality => { :min => 0 }
|
143
|
+
end
|
144
|
+
|
145
|
+
class ::Programmer < ActiveRecord::Base
|
146
|
+
inherits_from :employee
|
147
|
+
end
|
148
|
+
|
149
|
+
class ::Manager < ActiveRecord::Base
|
150
|
+
inherits_from :employee
|
151
|
+
validates :bonus, :presence => true, :numericality => true
|
152
|
+
end
|
153
|
+
|
154
|
+
Programmer.create(:first_name => 'Bob', :last_name => 'Jones', :salary => -50)
|
155
|
+
# fails because :salary must be >= 0
|
156
|
+
|
157
|
+
Manager.create!(:first_name => 'Bob', :last_name => 'Jones', :salary => 75000)
|
158
|
+
# fails because :bonus is not present
|
159
|
+
|
160
|
+
Mass Assignment Security
|
161
|
+
------------------------
|
162
|
+
|
163
|
+
Mass assignment security can optionally be used in the same manner you would
|
164
|
+
for a normal ActiveRecord model.
|
165
|
+
|
166
|
+
class Employee < ActiveRecord::Base
|
167
|
+
acts_as_superclass
|
168
|
+
attr_accessible :first_name, :last_name, :salary
|
169
|
+
end
|
170
|
+
|
171
|
+
class Manager < ActiveRecord::Base
|
172
|
+
inherits_from :employee
|
173
|
+
attr_accessible :bonus
|
174
|
+
end
|
175
|
+
|
176
|
+
**NOTE:** When an ActiveRecord model does not make a call to `attr_accessible`,
|
177
|
+
all its fields are presumed to be accessible. Currently, when using
|
178
|
+
MultipleTableInheritance, if a parent class does not call `attr_accesible` and
|
179
|
+
one of its children does, then the parent's attributes cannot properly be
|
180
|
+
stored. This will be fixed in a future release.
|
181
|
+
|
182
|
+
Associations
|
183
|
+
------------
|
184
|
+
|
185
|
+
Associations will also work in the same way as other attributes.
|
186
|
+
|
187
|
+
class Team < ActiveRecord::Base
|
188
|
+
attr_accessible :name
|
189
|
+
end
|
190
|
+
|
191
|
+
class Employee < ActiveRecord::Base
|
192
|
+
acts_as_superclass
|
193
|
+
belongs_to :team
|
194
|
+
end
|
195
|
+
|
196
|
+
class ::Programmer < ActiveRecord::Base
|
197
|
+
inherits_from :employee
|
198
|
+
has_many :known_languages
|
199
|
+
has_many :languages, :through => :known_languages
|
200
|
+
end
|
201
|
+
|
202
|
+
class ::Language < ActiveRecord::Base
|
203
|
+
attr_accessible :name
|
204
|
+
has_many :known_languages
|
205
|
+
has_many :programmers, :through => :known_languages
|
206
|
+
end
|
207
|
+
|
208
|
+
class ::KnownLanguage < ActiveRecord::Base
|
209
|
+
belongs_to :programmer
|
210
|
+
belongs_to :language
|
211
|
+
end
|
212
|
+
|
213
|
+
programmer = Programmer.first # <Programmer employee_id: 1 training_completed_at: "2009-03-06 00:30:00">
|
214
|
+
programmer.languages.collect(&:name) # ['Java', 'C++']
|
215
|
+
programmer.team.name # 'Website Front-End'
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
RSpec::Core::RakeTask.new('spec')
|
4
|
+
|
5
|
+
desc 'Default: run unit specs.'
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
# require 'rake'
|
9
|
+
# require 'rake/testtask'
|
10
|
+
# require 'rake/rdoctask'
|
11
|
+
#
|
12
|
+
# desc 'Default: run unit tests.'
|
13
|
+
# task :default => :test
|
14
|
+
#
|
15
|
+
# desc 'Test the multiple_table_inheritance plugin.'
|
16
|
+
# Rake::TestTask.new(:test) do |t|
|
17
|
+
# t.libs << 'lib'
|
18
|
+
# t.libs << 'test'
|
19
|
+
# t.pattern = 'test/**/*_test.rb'
|
20
|
+
# t.verbose = true
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# desc 'Generate documentation for the multiple_table_inheritance plugin.'
|
24
|
+
# Rake::RDocTask.new(:rdoc) do |rdoc|
|
25
|
+
# rdoc.rdoc_dir = 'rdoc'
|
26
|
+
# rdoc.title = 'MultipleTableInheritance'
|
27
|
+
# rdoc.options << '--line-numbers' << '--inline-source'
|
28
|
+
# rdoc.rdoc_files.include('README')
|
29
|
+
# rdoc.rdoc_files.include('lib/**/*.rb')
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# begin
|
33
|
+
# require 'jeweler'
|
34
|
+
# Jeweler::Tasks.new do |gem|
|
35
|
+
# gem.name = "multiple_table_inheritance"
|
36
|
+
# gem.summary = "ActiveRecord plugin designed to allow simple multiple table inheritance."
|
37
|
+
# gem.description = "ActiveRecord plugin designed to allow simple multiple table inheritance."
|
38
|
+
# gem.email = "tvdeyen@gmail.com"
|
39
|
+
# gem.homepage = "http://github.com/tvdeyen/multiple_table_inheritance"
|
40
|
+
# gem.authors = `git log --pretty=format:"%an"`.split("\n").uniq.sort
|
41
|
+
# gem.add_dependency "activerecord", ">=3.0.0"
|
42
|
+
# end
|
43
|
+
# Jeweler::GemcutterTasks.new
|
44
|
+
# rescue LoadError
|
45
|
+
# puts "Jeweler (or a dependency) not available."
|
46
|
+
# puts "Install it with: gem install jeweler"
|
47
|
+
# end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module MultipleTableInheritance
|
2
|
+
module ActiveRecord
|
3
|
+
module Child
|
4
|
+
def self.default_options
|
5
|
+
{ :dependent => :destroy, :inherit_methods => false }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
10
|
+
base.class_attribute :parent_association_name
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def inherits_from(association_name, options={})
|
15
|
+
# Standardize options, and remove those that should not affect the belongs_to relationship
|
16
|
+
options = Child::default_options.merge(options.to_options.reject { |k,v| v.nil? })
|
17
|
+
inherit_methods = options.delete(:inherit_methods)
|
18
|
+
|
19
|
+
include InstanceMethods
|
20
|
+
include DelegateMethods if inherit_methods
|
21
|
+
|
22
|
+
# Set association references.
|
23
|
+
self.parent_association_name = association_name.to_sym
|
24
|
+
self.primary_key = "#{parent_association_name}_id"
|
25
|
+
|
26
|
+
# Ensure parent association is always returned.
|
27
|
+
define_method("#{parent_association_name}_with_autobuild") do
|
28
|
+
send("#{parent_association_name}_without_autobuild") || send("build_#{parent_association_name}")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Allow getting and setting of parent attributes and relationships.
|
32
|
+
inherited_columns_and_associations.each do |name|
|
33
|
+
delegate name, "#{name}=", :to => parent_association_name
|
34
|
+
end
|
35
|
+
|
36
|
+
# Ensure parent's accessible attributes are accessible in child.
|
37
|
+
parent_association_class.accessible_attributes.each do |attr|
|
38
|
+
attr_accessible attr.to_sym
|
39
|
+
end
|
40
|
+
|
41
|
+
# Bind relationship, handle validation, and save properly.
|
42
|
+
belongs_to parent_association_name, options
|
43
|
+
alias_method_chain parent_association_name, :autobuild
|
44
|
+
before_validation :set_association_subtype
|
45
|
+
validate :parent_association_must_be_valid
|
46
|
+
before_save :parent_association_must_be_saved
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def parent_association_class
|
52
|
+
reflection = create_reflection(:belongs_to, parent_association_name, {}, self)
|
53
|
+
reflection.class_name.constantize
|
54
|
+
end
|
55
|
+
|
56
|
+
def inherited_columns_and_associations
|
57
|
+
# Get the associated columns and relationship names
|
58
|
+
inherited_columns = parent_association_class.column_names
|
59
|
+
inherited_methods = parent_association_class.reflections.map { |key, value| key.to_s }
|
60
|
+
|
61
|
+
# Filter out columns that the class already has
|
62
|
+
# inherited_columns = inherited_columns.reject do |column|
|
63
|
+
# (self.column_names.grep(column).length > 0) || (column == 'type') || (column == parent_association_class.subtype_column)
|
64
|
+
# end
|
65
|
+
|
66
|
+
# Filter out methods that the class already has
|
67
|
+
inherited_methods = inherited_methods.reject do |method|
|
68
|
+
self.reflections.map { |key, value| key.to_s }.include?(method)
|
69
|
+
end
|
70
|
+
|
71
|
+
inherited_columns + inherited_methods - ['id']
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
module InstanceMethods
|
76
|
+
private
|
77
|
+
|
78
|
+
def parent_association
|
79
|
+
send(self.class.parent_association_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def set_association_subtype
|
83
|
+
association = parent_association
|
84
|
+
if association.attribute_names.include?(association.class.subtype_column)
|
85
|
+
association[association.class.subtype_column] = self.class.to_s
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def parent_association_must_be_valid
|
90
|
+
association = parent_association
|
91
|
+
|
92
|
+
unless valid = association.valid?
|
93
|
+
association.errors.each do |attr, message|
|
94
|
+
errors.add(attr, message)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
valid
|
99
|
+
end
|
100
|
+
|
101
|
+
def parent_association_must_be_saved
|
102
|
+
association = parent_association
|
103
|
+
association.save(:validate => false)
|
104
|
+
self.id = association.id
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module DelegateMethods
|
109
|
+
private
|
110
|
+
|
111
|
+
def method_missing(name, *args)
|
112
|
+
if parent_association.respond_to?(name)
|
113
|
+
parent_association.public_send(name, *args)
|
114
|
+
else
|
115
|
+
super(name, *args)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def respond_to?(name)
|
120
|
+
parent_association.respond_to?(name) || super
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module MultipleTableInheritance
|
2
|
+
module ActiveRecord
|
3
|
+
module Child
|
4
|
+
def self.default_options
|
5
|
+
{ :dependent => :destroy, :inherit_methods => false }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def inherits_from(association_name, options={})
|
14
|
+
# Standardize parameters.
|
15
|
+
association_name = association_name.to_sym
|
16
|
+
options = Child::default_options.merge(options.to_options.reject { |k,v| v.nil? })
|
17
|
+
|
18
|
+
# Remove options that should not affect the belongs_to relationship.
|
19
|
+
inherit_methods = options.delete(:inherit_methods)
|
20
|
+
|
21
|
+
# Add an association, and set the foreign key.
|
22
|
+
belongs_to association_name, options
|
23
|
+
|
24
|
+
# Set the primary key since the inheriting table includes no `id` column.
|
25
|
+
self.primary_key = "#{association_name}_id"
|
26
|
+
|
27
|
+
# Always return an instance of the parent class, whether it be an existing or new instance.
|
28
|
+
always_return_association(association_name)
|
29
|
+
|
30
|
+
# Ensure the parent specifies the current class as the subtype.
|
31
|
+
associate_before_validation(association_name)
|
32
|
+
|
33
|
+
# Bind the validation of association.
|
34
|
+
validate_association(association_name)
|
35
|
+
|
36
|
+
# Ensure both the parent and the child records are saved.
|
37
|
+
save_association(association_name)
|
38
|
+
|
39
|
+
# Create proxy methods for instances of this class.
|
40
|
+
create_instance_methods(association_name)
|
41
|
+
|
42
|
+
# Delegate all missing method calls to the parent association if preferred.
|
43
|
+
delegate_parent_methods(association_name) if inherit_methods
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def always_return_association(association_name)
|
49
|
+
define_method("#{association_name}_with_autobuild") do
|
50
|
+
send("#{association_name}_without_autobuild") || send("build_#{association_name}")
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method_chain association_name, :autobuild
|
54
|
+
end
|
55
|
+
|
56
|
+
def associate_before_validation(association_name)
|
57
|
+
define_method(:set_subtype) do
|
58
|
+
association = send(association_name)
|
59
|
+
if association.attribute_names.include?(association.class.subtype_column)
|
60
|
+
association[association.class.subtype_column] = self.class.to_s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
before_validation :set_subtype
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate_association(association_name)
|
68
|
+
define_method(:parent_association_must_be_valid) do
|
69
|
+
association = send(association_name)
|
70
|
+
|
71
|
+
unless valid = association.valid?
|
72
|
+
association.errors.each do |attr, message|
|
73
|
+
errors.add(attr, message)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
valid
|
78
|
+
end
|
79
|
+
|
80
|
+
validate :parent_association_must_be_valid
|
81
|
+
end
|
82
|
+
|
83
|
+
def save_association(association_name)
|
84
|
+
define_method(:parent_association_must_be_saved) do
|
85
|
+
association = send(association_name)
|
86
|
+
association.save(:validate => false)
|
87
|
+
self.id = association.id
|
88
|
+
end
|
89
|
+
|
90
|
+
before_save :parent_association_must_be_saved
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_instance_methods(association_name)
|
94
|
+
inherited_columns_and_methods(association_name).each do |name|
|
95
|
+
delegate name, "#{name}=", :to => association_name
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def inherited_columns_and_methods(association_name)
|
100
|
+
# Get the class of association by reflection
|
101
|
+
reflection = create_reflection(:belongs_to, association_name, {}, self)
|
102
|
+
association_class = reflection.class_name.constantize
|
103
|
+
inherited_columns = association_class.column_names
|
104
|
+
inherited_methods = association_class.reflections.map { |key, value| key.to_s }
|
105
|
+
|
106
|
+
# Filter out columns that the class already has
|
107
|
+
inherited_columns = inherited_columns.reject do |column|
|
108
|
+
(self.column_names.grep(column).length > 0) || (column == 'type') || (column == association_class.subtype_column)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Filter out columns that the class already has
|
112
|
+
inherited_methods = inherited_methods.reject do |method|
|
113
|
+
self.reflections.map { |key, value| key.to_s }.include?(method)
|
114
|
+
end
|
115
|
+
|
116
|
+
inherited_columns + inherited_methods - ['id']
|
117
|
+
end
|
118
|
+
|
119
|
+
def delegate_parent_methods(association_name)
|
120
|
+
define_method("method_missing") do |name, *args|
|
121
|
+
association = self.public_send(association_name)
|
122
|
+
if association.present? && association.respond_to?(name)
|
123
|
+
association.send(name, *args)
|
124
|
+
else
|
125
|
+
super(name, *args)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module MultipleTableInheritance
|
2
|
+
module ActiveRecord
|
3
|
+
module Migration
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
alias_method_chain :create_table, :inherits
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Generate the association field.
|
11
|
+
def create_table_with_inherits(table_name, options = {}, &block)
|
12
|
+
options[:primary_key] = "#{options[:inherits]}_id" if options[:inherits]
|
13
|
+
|
14
|
+
create_table_without_inherits(table_name, options) do |table_defintion|
|
15
|
+
yield table_defintion
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module MultipleTableInheritance
|
2
|
+
module ActiveRecord
|
3
|
+
module Parent
|
4
|
+
def self.default_options
|
5
|
+
{ :subtype => 'subtype' }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
base.class_attribute :subtype_column
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def acts_as_superclass(options={})
|
15
|
+
options = Parent::default_options.merge(options.to_options.reject { |k,v| v.nil? })
|
16
|
+
self.subtype_column = options[:subtype]
|
17
|
+
|
18
|
+
if self.column_names.include?(subtype_column.to_s)
|
19
|
+
class << self
|
20
|
+
alias_method_chain :find_by_sql, :inherits
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def find_by_sql_with_inherits(*args)
|
28
|
+
parent_records = find_by_sql_without_inherits(*args)
|
29
|
+
child_records = []
|
30
|
+
|
31
|
+
# find all child records
|
32
|
+
ids_by_type(parent_records).each do |type, ids|
|
33
|
+
begin
|
34
|
+
klass = type.constantize
|
35
|
+
child_records += klass.find(ids)
|
36
|
+
rescue NameError => e
|
37
|
+
# TODO log error
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# associate the parent records with the child records to reduce SQL calls and prevent recursion
|
42
|
+
child_records.each do |child|
|
43
|
+
association_name = to_s.demodulize.underscore
|
44
|
+
parent = parent_records.find { |parent| child.id == parent.id }
|
45
|
+
child.send("#{association_name}=", parent)
|
46
|
+
end
|
47
|
+
|
48
|
+
# TODO order the child_records array to match the order of the parent_records array (by comparing id's)
|
49
|
+
|
50
|
+
child_records
|
51
|
+
end
|
52
|
+
|
53
|
+
def ids_by_type(records)
|
54
|
+
subtypes = records.collect(&self.subtype_column.to_sym).uniq
|
55
|
+
subtypes = subtypes.collect do |subtype|
|
56
|
+
subtype_records = records.select { |record| record[subtype_column.to_sym] == subtype}
|
57
|
+
subtype_ids = subtype_records.collect { |record| record.id }
|
58
|
+
[subtype, subtype_ids]
|
59
|
+
end
|
60
|
+
Hash[subtypes]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module MultipleTableInheritance
|
2
|
+
if defined?(Rails::Railtie)
|
3
|
+
class Railtie < Rails::Railtie
|
4
|
+
initializer 'multiple_table_inheritance.insert_into_active_record' do
|
5
|
+
::ActiveSupport.on_load(:active_record) { Railtie.insert }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Railtie
|
11
|
+
def self.insert
|
12
|
+
::ActiveRecord::Base.module_eval do
|
13
|
+
include MultipleTableInheritance::ActiveRecord::Child
|
14
|
+
include MultipleTableInheritance::ActiveRecord::Parent
|
15
|
+
end
|
16
|
+
|
17
|
+
::ActiveRecord::ConnectionAdapters::SchemaStatements.module_eval do
|
18
|
+
include MultipleTableInheritance::ActiveRecord::Migration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "multiple_table_inheritance/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "multiple_table_inheritance"
|
7
|
+
s.version = MultipleTableInheritance::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Matt Huggins"]
|
10
|
+
s.email = ["matt@matthuggins.com"]
|
11
|
+
s.homepage = "http://github.com/mhuggins/multiple_table_inheritance"
|
12
|
+
s.summary = "ActiveRecord plugin designed to allow simple multiple table inheritance."
|
13
|
+
s.description = "ActiveRecord plugin designed to allow simple multiple table inheritance."
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
21
|
+
s.extra_rdoc_files = ["README.md"]
|
22
|
+
|
23
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
24
|
+
s.rubygems_version = %q{0.1.0}
|
25
|
+
|
26
|
+
s.add_dependency('activerecord', '>= 3.0.0')
|
27
|
+
s.add_dependency('activesupport', '>= 3.0.0')
|
28
|
+
s.add_development_dependency('rspec-rails', '~> 2.8.0')
|
29
|
+
s.add_development_dependency('rspec_tag_matchers', '>= 1.0.0')
|
30
|
+
s.add_development_dependency('sqlite3-ruby', '>= 1.3.3')
|
31
|
+
s.add_development_dependency('database_cleaner', '>= 0.7.1')
|
32
|
+
# s.add_development_dependency('appraisal')
|
33
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MultipleTableInheritance::ActiveRecord::Child do
|
4
|
+
context 'retrieving records' do
|
5
|
+
it 'should only fetch subtypes' do
|
6
|
+
subtypes = Employee.all.collect(&:subtype).uniq
|
7
|
+
subtypes.each do |subtype|
|
8
|
+
['Programmer', 'Manager'].should include(subtype)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should fetch all child records' do
|
13
|
+
pending "find_by_sql_without_inherit.size.should == find_by_sql_with_inherit.size"
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'an invalid subtype is included' do
|
17
|
+
it 'should return successfully' do
|
18
|
+
pending "error should not be thrown"
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should generate log entry' do
|
22
|
+
pending "logger.error should have been called"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should maintain result order' do
|
27
|
+
pending "find_by_sql_without_inherit.collect(&:id).should == find_by_sql_with_inherit.collect(&:id)"
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'default subtype used' do
|
31
|
+
pending "test_everything"
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'custom subtype used' do
|
35
|
+
pending "test_everything"
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should work with namespaced classes' do
|
39
|
+
pending "test_everything"
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should inherit parent methods' do
|
43
|
+
pending 'todo'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'creating records' do
|
48
|
+
pending "todo"
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'updating records' do
|
52
|
+
pending "todo"
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'deleting records' do
|
56
|
+
pending "todo"
|
57
|
+
end
|
58
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'database_cleaner'
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '../lib/multiple_table_inheritance'))
|
4
|
+
require 'support/tables'
|
5
|
+
require 'support/classes'
|
6
|
+
|
7
|
+
module MultipleTableInheritanceSpecHelper
|
8
|
+
def mock_everything
|
9
|
+
@team = Team.create!(:name => 'Website')
|
10
|
+
@java = Language.create!(:name => 'Java')
|
11
|
+
@cpp = Language.create!(:name => 'C++')
|
12
|
+
@programmer = Programmer.create!(
|
13
|
+
:first_name => 'Mario',
|
14
|
+
:last_name => 'Mario',
|
15
|
+
:salary => 65000,
|
16
|
+
:team => @team,
|
17
|
+
:languages => [@java, @cpp]) # programmer-specific relationship
|
18
|
+
@manager = Manager.create!(
|
19
|
+
:first_name => 'King',
|
20
|
+
:last_name => 'Koopa',
|
21
|
+
:salary => 70000,
|
22
|
+
:team => @team,
|
23
|
+
:bonus => 5000) # manager-specific field
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
28
|
+
RSpec.configure do |config|
|
29
|
+
include MultipleTableInheritanceSpecHelper
|
30
|
+
|
31
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
32
|
+
config.run_all_when_everything_filtered = true
|
33
|
+
config.filter_run :focus
|
34
|
+
|
35
|
+
config.before(:suite) do
|
36
|
+
DatabaseCleaner.strategy = :transaction
|
37
|
+
DatabaseCleaner.clean_with(:truncation)
|
38
|
+
end
|
39
|
+
|
40
|
+
config.before(:each) do
|
41
|
+
DatabaseCleaner.start
|
42
|
+
mock_everything
|
43
|
+
end
|
44
|
+
|
45
|
+
config.after(:each) do
|
46
|
+
DatabaseCleaner.clean
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class ::Employee < ActiveRecord::Base
|
2
|
+
acts_as_superclass
|
3
|
+
attr_accessible :first_name, :last_name, :salary
|
4
|
+
belongs_to :team
|
5
|
+
validates :first_name, :presence => true
|
6
|
+
validates :last_name, :presence => true
|
7
|
+
validates :salary, :presence => true, :numericality => { :min => 0 }
|
8
|
+
end
|
9
|
+
|
10
|
+
class ::Programmer < ActiveRecord::Base
|
11
|
+
inherits_from :employee
|
12
|
+
has_many :known_languages
|
13
|
+
has_many :languages, :through => :known_languages
|
14
|
+
end
|
15
|
+
|
16
|
+
class ::Manager < ActiveRecord::Base
|
17
|
+
inherits_from :employee
|
18
|
+
attr_accessible :bonus
|
19
|
+
validates :bonus, :numericality => true
|
20
|
+
end
|
21
|
+
|
22
|
+
class ::Team < ActiveRecord::Base
|
23
|
+
attr_accessible :name, :description
|
24
|
+
has_many :employees
|
25
|
+
validates :name, :presence => true, :uniqueness => true
|
26
|
+
end
|
27
|
+
|
28
|
+
class ::Language < ActiveRecord::Base
|
29
|
+
attr_accessible :name
|
30
|
+
has_many :known_languages
|
31
|
+
has_many :programmers, :through => :known_languages
|
32
|
+
validates :name, :presence => true, :uniqueness => true
|
33
|
+
end
|
34
|
+
|
35
|
+
class ::KnownLanguage < ActiveRecord::Base
|
36
|
+
belongs_to :programmer
|
37
|
+
belongs_to :language
|
38
|
+
validates :programmer_id, :presence => true
|
39
|
+
validates :language_id, :presence => true, :uniqueness => { :scope => :programmer_id }
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'multiple_table_inheritance/railtie'
|
2
|
+
|
3
|
+
MultipleTableInheritance::Railtie.insert
|
4
|
+
|
5
|
+
ActiveRecord::Base.establish_connection(
|
6
|
+
:adapter => 'sqlite3',
|
7
|
+
:database => File.expand_path(File.join(File.dirname(__FILE__), '../../db/multiple_table_inheritance.db'))
|
8
|
+
)
|
9
|
+
|
10
|
+
ActiveRecord::Base.connection do
|
11
|
+
['employees', 'programmers', 'managers', 'teams', 'languages', 'known_languages'].each do |table|
|
12
|
+
execute "DROP TABLE IF EXISTS '#{table}'"
|
13
|
+
end
|
14
|
+
|
15
|
+
create_table :employees do |t|
|
16
|
+
t.string :subtype, :null => false
|
17
|
+
t.string :first_name, :null => false
|
18
|
+
t.string :last_name, :null => false
|
19
|
+
t.integer :salary, :null => false
|
20
|
+
t.integer :team_id
|
21
|
+
end
|
22
|
+
|
23
|
+
create_table :programmers, :inherits => :employee do |t|
|
24
|
+
end
|
25
|
+
|
26
|
+
create_table :managers, :inherits => :employee do |t|
|
27
|
+
t.integer :bonus
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table :teams do |t|
|
31
|
+
t.string :name, :null => false
|
32
|
+
end
|
33
|
+
|
34
|
+
create_table :languages do |t|
|
35
|
+
t.string :name, :null => false
|
36
|
+
end
|
37
|
+
|
38
|
+
create_table :known_languages do |t|
|
39
|
+
t.integer :programmer_id, :null => false
|
40
|
+
t.integer :language_id, :null => false
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: multiple_table_inheritance
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matt Huggins
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-06 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: &2153063820 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2153063820
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activesupport
|
27
|
+
requirement: &2153063240 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.0.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2153063240
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec-rails
|
38
|
+
requirement: &2153062780 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.8.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2153062780
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec_tag_matchers
|
49
|
+
requirement: &2153062140 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.0.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2153062140
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: sqlite3-ruby
|
60
|
+
requirement: &2153061480 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.3.3
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2153061480
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: database_cleaner
|
71
|
+
requirement: &2153060780 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 0.7.1
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *2153060780
|
80
|
+
description: ActiveRecord plugin designed to allow simple multiple table inheritance.
|
81
|
+
email:
|
82
|
+
- matt@matthuggins.com
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files:
|
86
|
+
- README.md
|
87
|
+
files:
|
88
|
+
- .rspec
|
89
|
+
- Gemfile
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- lib/multiple_table_inheritance.rb
|
93
|
+
- lib/multiple_table_inheritance/active_record.rb
|
94
|
+
- lib/multiple_table_inheritance/active_record/child.rb
|
95
|
+
- lib/multiple_table_inheritance/active_record/child.rb.bak
|
96
|
+
- lib/multiple_table_inheritance/active_record/migration.rb
|
97
|
+
- lib/multiple_table_inheritance/active_record/parent.rb
|
98
|
+
- lib/multiple_table_inheritance/railtie.rb
|
99
|
+
- lib/multiple_table_inheritance/version.rb
|
100
|
+
- multiiple_table_inheritance.gemspec
|
101
|
+
- spec/active_record/child_spec.rb
|
102
|
+
- spec/active_record/parent_spec.rb
|
103
|
+
- spec/spec_helper.rb
|
104
|
+
- spec/support/classes.rb
|
105
|
+
- spec/support/tables.rb
|
106
|
+
homepage: http://github.com/mhuggins/multiple_table_inheritance
|
107
|
+
licenses: []
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options:
|
110
|
+
- --charset=UTF-8
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 1.8.10
|
128
|
+
signing_key:
|
129
|
+
specification_version: 3
|
130
|
+
summary: ActiveRecord plugin designed to allow simple multiple table inheritance.
|
131
|
+
test_files:
|
132
|
+
- spec/active_record/child_spec.rb
|
133
|
+
- spec/active_record/parent_spec.rb
|
134
|
+
- spec/spec_helper.rb
|
135
|
+
- spec/support/classes.rb
|
136
|
+
- spec/support/tables.rb
|