multiple_table_inheritance 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|