activerecord-cti 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -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).
@@ -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,8 @@
1
+ require "activerecord/cti/railtie"
2
+ require "activerecord/cti/base_class"
3
+ require "activerecord/cti/sub_class"
4
+
5
+ module ActiveRecord
6
+ module Cti
7
+ end
8
+ end
@@ -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,6 @@
1
+ module Activerecord
2
+ module Cti
3
+ class Railtie < ::Rails::Railtie
4
+ end
5
+ end
6
+ 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
@@ -0,0 +1,5 @@
1
+ module Activerecord
2
+ module Cti
3
+ VERSION = '0.1.1'
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :activerecord_cti do
3
+ # # Task goes here
4
+ # 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: []