activerecord-cti 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +115 -0
- data/Rakefile +27 -0
- data/lib/activerecord/cti.rb +8 -0
- data/lib/activerecord/cti/base_class.rb +18 -0
- data/lib/activerecord/cti/railtie.rb +6 -0
- data/lib/activerecord/cti/sub_class.rb +177 -0
- data/lib/activerecord/cti/version.rb +5 -0
- data/lib/tasks/activerecord/cti_tasks.rake +4 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1ae47e6580bbe7ed9941cce85c4233042889acbdb1ec1ebcffcfa5b9383b383b
|
4
|
+
data.tar.gz: 21320a42fe6ae3f20e6805ee6975c6e316a16486f358a01a5daecf7cf216c588
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0ff27306a2099339cef9f29bda3e6b6dad3e0edfc48c1b91fa36d5e53385a5563561f0529a635495c098cada33cb3bde9476cd19eee0e00d190489f8d4e5813d
|
7
|
+
data.tar.gz: 8c9d4683555a12478cb779ae042b08233975b55513d5a4ad64023738526fc8b3510065940bf9f87867fc2ee7497cbabad758555646d3b885066a8991b747435c
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2020
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# ActiveRecord::Cti
|
2
|
+
ActiveRecord-Cti is a library implemented [Class Table Inheritance](https://martinfowler.com/eaaCatalog/classTableInheritance.html) on ActiveRecord.
|
3
|
+
Class Table Inheritance (CTI) is useful under the circumstances that an ActiveRecord object is in multiple positions or has multiple roles, and you want to describe it's structure on the database.
|
4
|
+
For Example, one person may be a player and a coach in a soccer team.
|
5
|
+
|
6
|
+
## Why use activerecord-cti ?
|
7
|
+
In ActiveRecord, Single Table Inheritance(STI) is implemented as a method of how to express inheritance model on database. Class Table Inheritance (CTI) has more powerful and flexible expressiveness for inheritance model on database than it of STI.
|
8
|
+
|
9
|
+
For Example, Suppose you want to describe the following class structure on database.
|
10
|
+
|
11
|
+
![Class Diagram](public/images/class_diagram.png)
|
12
|
+
|
13
|
+
But STI has a disadvantage that it is not possible to represent one record as an object of two different models at the same time.
|
14
|
+
|
15
|
+
#### people talbe (STI)
|
16
|
+
| id | type | name | birth_year | position_name | licence_name |
|
17
|
+
|----|------|-----------|------------|-----------------|---------------|
|
18
|
+
| 1 |Player| Ryan Giggs| 1973 | midfielder | |
|
19
|
+
| 2 |Coach | Ryan Giggs| 1973 | | UEFA Pro |
|
20
|
+
|
21
|
+
As mentiond above, for expressing two `Person`'s subclasses objects, which are `Player` and `Coach`, you have to insert two records into people table in STI.
|
22
|
+
It is cursed that the contents of `name` and `birth_year` columns are duplicated and `position_name` and `licence_name` columns are sparse.
|
23
|
+
|
24
|
+
CTI can solve these problems by using multiple related tables like shown below, literally for class table inheritance.
|
25
|
+
|
26
|
+
![ER Diagram](public/images/er_diagram.png)
|
27
|
+
|
28
|
+
## How to use
|
29
|
+
### Preparation
|
30
|
+
First of all, generate the files of models you want to apply CTI to, and execute migration.
|
31
|
+
|
32
|
+
```bash
|
33
|
+
$ rails g model Person name:string birth_year:integer
|
34
|
+
$ rails g model Player person_id:integer position_name:string
|
35
|
+
$ rails g model Coach person_id:integer licence_name:string
|
36
|
+
$ rake db:migrate
|
37
|
+
```
|
38
|
+
|
39
|
+
Next, add the following line into `Person` model, which is base class.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
class Person < ApplicationRecord
|
43
|
+
include ActiveRecord::Cti::BaseClass #added
|
44
|
+
end
|
45
|
+
```
|
46
|
+
By this mix-in, `Person` model is configured as base class in CTI, and automatically becomes abstract class as well.
|
47
|
+
|
48
|
+
And then, rewrite files of subclass models for inheriting base class.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
class Player < Person
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class Coach < Person
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
### Coding
|
61
|
+
To save data of Ryan Giggs as a football player, describe following:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
player = Player.new(
|
65
|
+
name: 'Ryan Giggs',
|
66
|
+
birth_year: 1973,
|
67
|
+
position_name: 'midfielder'
|
68
|
+
)
|
69
|
+
player.save
|
70
|
+
```
|
71
|
+
So that his data is automatically split into two related tables.
|
72
|
+
```bash
|
73
|
+
MariaDB> SELECT * FROM people limit 1;
|
74
|
+
+----+----------------+------------+
|
75
|
+
| id | name | birth_year |
|
76
|
+
+----+----------------+------------+
|
77
|
+
| 1 | Ryan Giggs | 1973 |
|
78
|
+
+----+----------------+------------+
|
79
|
+
|
80
|
+
MariaDB> SELECT * FROM players limit 1;
|
81
|
+
+----+----------------+---------------+
|
82
|
+
| id | person_id | position_name |
|
83
|
+
+----+----------------+---------------+
|
84
|
+
| 1 | 1 | midfielder |
|
85
|
+
+----+----------------+---------------+
|
86
|
+
```
|
87
|
+
|
88
|
+
Then, Ryan Giggs started coaching at Manchester United in 2013 as well as being a player.
|
89
|
+
To save the data of Giggs as a coach to DB, describe following:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
player = Player.find_by(name: 'Ryan Giggs')
|
93
|
+
coach = player.to_coach(licence_name: 'UEFA Pro')
|
94
|
+
coach.save
|
95
|
+
```
|
96
|
+
|
97
|
+
So that his data is newly inserted into only coaches table.
|
98
|
+
```bash
|
99
|
+
MariaDB> SELECT * FROM coaches limit 1;
|
100
|
+
+----+----------------+--------------+
|
101
|
+
| id | person_id | licence_name |
|
102
|
+
+----+----------------+--------------+
|
103
|
+
| 1 | 1 | UEFA Pro |
|
104
|
+
+----+----------------+--------------+
|
105
|
+
```
|
106
|
+
|
107
|
+
To get Ryan Giggs's Data as a coach, describe following:
|
108
|
+
```ruby
|
109
|
+
Coach.find_by_name('Ryan Giggs') #<Coach id: 1, name: "Ryan Giggs", licence_name: 'UEFA Pro'>
|
110
|
+
```
|
111
|
+
|
112
|
+
Like this, pserson_id, which coaches table has as foreign_key reffered to base class object, is concealed.
|
113
|
+
|
114
|
+
## License
|
115
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Activerecord::Cti'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'bundler/gem_tasks'
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
t.libs << 'test'
|
23
|
+
t.pattern = 'test/**/*_test.rb'
|
24
|
+
t.verbose = false
|
25
|
+
end
|
26
|
+
|
27
|
+
task default: :test
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Cti
|
3
|
+
module BaseClass
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
self.abstract_class = true
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def inherited(subclass)
|
12
|
+
super
|
13
|
+
subclass.include(ActiveRecord::Cti::SubClass)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Cti
|
3
|
+
module SubClass
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
default_scope { joins("INNER JOIN #{superclass_table_name} ON #{table_name}.#{foreign_key_name} = #{superclass_table_name}.id").select(default_select_columns) }
|
8
|
+
|
9
|
+
# Define dinamically to_* methods, which convert self to other subclass has same CTI superclass.
|
10
|
+
Pathname.glob("#{Rails.root}/app/models/*").collect do
|
11
|
+
|path| path.basename.to_s.split('.').first.classify.safe_constantize
|
12
|
+
end.compact.delete_if do |model|
|
13
|
+
!model.superclass.include?(ActiveRecord::Cti::BaseClass) or model == self
|
14
|
+
end.each do |model|
|
15
|
+
define_method("to_#{model.to_s.underscore}") do |args = {}|
|
16
|
+
model_instance = model.new(args)
|
17
|
+
model_instance.attributes = attributes.slice(*superclass_for_rw.column_names - [@primary_key])
|
18
|
+
model_instance.send(:foreign_key_value=, foreign_key_value)
|
19
|
+
model_instance
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class_methods do
|
25
|
+
# Generates all the attribute related methods for columns in the database
|
26
|
+
# accessors, mutators and query methods.
|
27
|
+
def define_attribute_methods # :nodoc:
|
28
|
+
return false if @attribute_methods_generated
|
29
|
+
generated_attribute_methods.synchronize do
|
30
|
+
return false if @attribute_methods_generated
|
31
|
+
@attribute_methods_generated = true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def superclass_name
|
36
|
+
superclass.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
def superclass_table_name
|
40
|
+
superclass_name.tableize
|
41
|
+
end
|
42
|
+
|
43
|
+
def subclass_table_name
|
44
|
+
table_name
|
45
|
+
end
|
46
|
+
|
47
|
+
def foreign_key_name
|
48
|
+
superclass.to_s.foreign_key
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get columns to pass +joins+ while calling +default_scope+.
|
52
|
+
def default_select_columns
|
53
|
+
((superclass_column_names - [primary_key]).collect do |key|
|
54
|
+
"#{superclass_table_name}.#{key}"
|
55
|
+
end + subclass_column_names.collect do |key|
|
56
|
+
"#{subclass_table_name}.#{key}"
|
57
|
+
end).join(',')
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_by(*args)
|
61
|
+
unless subclass_column_names.include?(args.first.keys.first)
|
62
|
+
args = [{"#{superclass_table_name}.#{args.first.keys.first.to_s}": args.first.values.first}]
|
63
|
+
end
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
67
|
+
def where(opts = :chain, *rest)
|
68
|
+
unless subclass_column_names.include?(opts.keys.first)
|
69
|
+
opts = {"#{superclass_table_name}.#{opts.keys.first.to_s}": opts.values.first}
|
70
|
+
end
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def load_schema!
|
76
|
+
@columns_hash = superclass_columns_hash.merge(subclass_columns_hash)
|
77
|
+
@columns_hash.each do |name, column|
|
78
|
+
define_attribute(
|
79
|
+
name,
|
80
|
+
connection.lookup_cast_type_from_column(column),
|
81
|
+
default: column.default,
|
82
|
+
user_provided_default: false
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def superclass_columns_hash
|
88
|
+
connection.schema_cache.columns_hash(superclass_table_name).except(*superclass_ignored_columns)
|
89
|
+
end
|
90
|
+
|
91
|
+
def superclass_column_names
|
92
|
+
superclass_columns_hash.keys
|
93
|
+
end
|
94
|
+
|
95
|
+
def subclass_columns_hash
|
96
|
+
connection.schema_cache.columns_hash(subclass_table_name).except(*subclass_ignored_columns)
|
97
|
+
end
|
98
|
+
|
99
|
+
def subclass_column_names
|
100
|
+
subclass_columns_hash.keys
|
101
|
+
end
|
102
|
+
|
103
|
+
def superclass_ignored_columns
|
104
|
+
["created_at", "updated_at"]
|
105
|
+
end
|
106
|
+
|
107
|
+
def subclass_ignored_columns
|
108
|
+
[foreign_key_name]
|
109
|
+
end
|
110
|
+
end #end of class_methods
|
111
|
+
|
112
|
+
# To save into two related tables while inserting.
|
113
|
+
def save(*args, &block)
|
114
|
+
_superclass_instance_for_rw = superclass_instance_for_rw
|
115
|
+
_subclass_instance_for_rw = subclass_instance_for_rw
|
116
|
+
ActiveRecord::Base.transaction do
|
117
|
+
_superclass_instance_for_rw.send(:create_or_update)
|
118
|
+
_subclass_instance_for_rw.send("#{foreign_key_name}=", _superclass_instance_for_rw.id)
|
119
|
+
_subclass_instance_for_rw.send(:create_or_update)
|
120
|
+
end
|
121
|
+
self.id = _subclass_instance_for_rw.id
|
122
|
+
_superclass_instance_for_rw.id.present? and _subclass_instance_for_rw.id.present?
|
123
|
+
rescue ActiveRecord::RecordInvalid
|
124
|
+
false
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
def superclass_instance_for_rw(*args, &block)
|
129
|
+
if foreign_key_value.present?
|
130
|
+
superclass_instance_for_rw = superclass_for_rw.find(foreign_key_value)
|
131
|
+
superclass_instance_for_rw.attributes = attributes.slice(*superclass_for_rw.column_names - [@primary_key])
|
132
|
+
superclass_instance_for_rw
|
133
|
+
else
|
134
|
+
superclass_for_rw.new(attributes.slice(*superclass_for_rw.column_names), &block)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def subclass_instance_for_rw(*args, &block)
|
139
|
+
if self.id.present?
|
140
|
+
subclass_instance_for_rw = subclass_for_rw.find(self.id)
|
141
|
+
subclass_instance_for_rw.attributes = attributes.except(*superclass_for_rw.column_names)
|
142
|
+
subclass_instance_for_rw
|
143
|
+
else
|
144
|
+
subclass_for_rw.new(attributes.except(*superclass_for_rw.column_names), &block)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def foreign_key_name
|
149
|
+
self.class.foreign_key_name
|
150
|
+
end
|
151
|
+
|
152
|
+
def foreign_key_value
|
153
|
+
return @foreign_key_value if @foreign_key_value.present?
|
154
|
+
return nil if self.id.nil?
|
155
|
+
@foreign_key = subclass_for_rw.find(self.id)&.send(foreign_key_name)
|
156
|
+
end
|
157
|
+
|
158
|
+
def foreign_key_value=(value)
|
159
|
+
@foreign_key_value = value
|
160
|
+
end
|
161
|
+
|
162
|
+
def superclass_for_rw
|
163
|
+
table_name = self.class.superclass_table_name
|
164
|
+
@superclass_for_rw || @superclass_for_rw = Class.new(ActiveRecord::Base) do
|
165
|
+
self.table_name = table_name
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def subclass_for_rw
|
170
|
+
table_name = self.class.subclass_table_name
|
171
|
+
@subclass_for_rw || @subclass_for_rw = Class.new(ActiveRecord::Base) do
|
172
|
+
self.table_name = table_name
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-cti
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- khata
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-06-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.0.2
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 6.0.2.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 6.0.2
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 6.0.2.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: sqlite3
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
description: ActiveRecord-Cti is a library implemented Class Table Inheritance on
|
48
|
+
ActiveRecord. Class Table Inheritance (CTI) is useful under the circumstances that
|
49
|
+
an ActiveRecord object is in multiple positions or has multiple roles, and you want
|
50
|
+
to describe it's structure on the database. For Example, one person may be a player
|
51
|
+
and a coach in a soccer team.
|
52
|
+
email:
|
53
|
+
- hata_kentaro_es@tokushima-inc.jp
|
54
|
+
executables: []
|
55
|
+
extensions: []
|
56
|
+
extra_rdoc_files: []
|
57
|
+
files:
|
58
|
+
- MIT-LICENSE
|
59
|
+
- README.md
|
60
|
+
- Rakefile
|
61
|
+
- lib/activerecord/cti.rb
|
62
|
+
- lib/activerecord/cti/base_class.rb
|
63
|
+
- lib/activerecord/cti/railtie.rb
|
64
|
+
- lib/activerecord/cti/sub_class.rb
|
65
|
+
- lib/activerecord/cti/version.rb
|
66
|
+
- lib/tasks/activerecord/cti_tasks.rake
|
67
|
+
homepage: https://bs.tokushima-inc.jp/
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
metadata: {}
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.0.3
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: ActiveRecord-Cti is a library implemented Class Table Inheritance on ActiveRecord.
|
90
|
+
test_files: []
|