quickery 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3bb77d2c32c2ce80c651fd671701fd561c4ba37d8920827804bf5d092cd5085e
4
+ data.tar.gz: 84388c8df61f76837adfd630b9c8a0421d818d5098b4d3966a547ee8408efd04
5
+ SHA512:
6
+ metadata.gz: 3f8f3efade73d92df662b8f43aaaab39af3b22390c83f3f2ff82ce292cdae737e451dbd83c1b5981d7272c5effbbb294288d823a6ea07002be3deb12277ff738
7
+ data.tar.gz: 1229a87bf58a64bcc2f2fe1e014bc4c96b28f07ebc537b0e73164a30c4775e515db4730c927957c133263a58097471a429bbc48d5399faed2bf546c689a4e2fa
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ /spec/internal/log/*
11
+ /spec/internal/tmp/*
12
+ *.sqlite
13
+ *.sqlite-journal
14
+
15
+ # rspec failure tracking
16
+ .rspec_status
17
+
18
+ *.gem
19
+ .byebug_history
20
+
21
+ Gemfile.lock
22
+ gemfiles/*.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install:
6
+ - gem install bundler -v 1.16.1
7
+ - bundle exec appraisal install
8
+ script:
9
+ - bundle exec appraisal rake
data/Appraisals ADDED
@@ -0,0 +1,7 @@
1
+ appraise 'rails-4' do
2
+ gem 'rails', '4.2.10'
3
+ end
4
+
5
+ appraise 'rails-5' do
6
+ gem 'rails', '5.2.1'
7
+ end
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ group :development, :test do
6
+ gem 'factory_bot_rails', require: false
7
+ end
8
+
9
+ # Specify your gem's dependencies in quickery.gemspec
10
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,163 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ quickery (0.1.0)
5
+ activerecord
6
+ activesupport
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionpack (5.2.0)
12
+ actionview (= 5.2.0)
13
+ activesupport (= 5.2.0)
14
+ rack (~> 2.0)
15
+ rack-test (>= 0.6.3)
16
+ rails-dom-testing (~> 2.0)
17
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
+ actionview (5.2.0)
19
+ activesupport (= 5.2.0)
20
+ builder (~> 3.1)
21
+ erubi (~> 1.4)
22
+ rails-dom-testing (~> 2.0)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
24
+ activemodel (5.2.0)
25
+ activesupport (= 5.2.0)
26
+ activerecord (5.2.0)
27
+ activemodel (= 5.2.0)
28
+ activesupport (= 5.2.0)
29
+ arel (>= 9.0)
30
+ activesupport (5.2.0)
31
+ concurrent-ruby (~> 1.0, >= 1.0.2)
32
+ i18n (>= 0.7, < 2)
33
+ minitest (~> 5.1)
34
+ tzinfo (~> 1.1)
35
+ appraisal (2.2.0)
36
+ bundler
37
+ rake
38
+ thor (>= 0.14.0)
39
+ arel (9.0.0)
40
+ builder (3.2.3)
41
+ byebug (9.1.0)
42
+ coderay (1.1.2)
43
+ combustion (0.9.1)
44
+ activesupport (>= 3.0.0)
45
+ railties (>= 3.0.0)
46
+ thor (>= 0.14.6)
47
+ concurrent-ruby (1.0.5)
48
+ crass (1.0.4)
49
+ database_cleaner (1.7.0)
50
+ diff-lcs (1.3)
51
+ erubi (1.7.1)
52
+ factory_bot (4.10.0)
53
+ activesupport (>= 3.0.0)
54
+ factory_bot_rails (4.10.0)
55
+ factory_bot (~> 4.10.0)
56
+ railties (>= 3.0.0)
57
+ faker (1.9.1)
58
+ i18n (>= 0.7)
59
+ ffi (1.9.25)
60
+ formatador (0.2.5)
61
+ guard (2.14.2)
62
+ formatador (>= 0.2.4)
63
+ listen (>= 2.7, < 4.0)
64
+ lumberjack (>= 1.0.12, < 2.0)
65
+ nenv (~> 0.1)
66
+ notiffany (~> 0.0)
67
+ pry (>= 0.9.12)
68
+ shellany (~> 0.0)
69
+ thor (>= 0.18.1)
70
+ guard-compat (1.2.1)
71
+ guard-rspec (4.7.3)
72
+ guard (~> 2.1)
73
+ guard-compat (~> 1.1)
74
+ rspec (>= 2.99.0, < 4.0)
75
+ i18n (1.0.1)
76
+ concurrent-ruby (~> 1.0)
77
+ listen (3.1.5)
78
+ rb-fsevent (~> 0.9, >= 0.9.4)
79
+ rb-inotify (~> 0.9, >= 0.9.7)
80
+ ruby_dep (~> 1.2)
81
+ loofah (2.2.2)
82
+ crass (~> 1.0.2)
83
+ nokogiri (>= 1.5.9)
84
+ lumberjack (1.0.13)
85
+ method_source (0.9.0)
86
+ mini_portile2 (2.3.0)
87
+ minitest (5.11.3)
88
+ nenv (0.3.0)
89
+ nokogiri (1.8.4)
90
+ mini_portile2 (~> 2.3.0)
91
+ notiffany (0.1.1)
92
+ nenv (~> 0.1)
93
+ shellany (~> 0.0)
94
+ pry (0.11.3)
95
+ coderay (~> 1.1.0)
96
+ method_source (~> 0.9.0)
97
+ rack (2.0.5)
98
+ rack-test (1.1.0)
99
+ rack (>= 1.0, < 3)
100
+ rails-dom-testing (2.0.3)
101
+ activesupport (>= 4.2.0)
102
+ nokogiri (>= 1.6)
103
+ rails-html-sanitizer (1.0.4)
104
+ loofah (~> 2.2, >= 2.2.2)
105
+ railties (5.2.0)
106
+ actionpack (= 5.2.0)
107
+ activesupport (= 5.2.0)
108
+ method_source
109
+ rake (>= 0.8.7)
110
+ thor (>= 0.18.1, < 2.0)
111
+ rake (10.5.0)
112
+ rb-fsevent (0.10.3)
113
+ rb-inotify (0.9.10)
114
+ ffi (>= 0.5.0, < 2)
115
+ rspec (3.7.0)
116
+ rspec-core (~> 3.7.0)
117
+ rspec-expectations (~> 3.7.0)
118
+ rspec-mocks (~> 3.7.0)
119
+ rspec-core (3.7.1)
120
+ rspec-support (~> 3.7.0)
121
+ rspec-expectations (3.7.0)
122
+ diff-lcs (>= 1.2.0, < 2.0)
123
+ rspec-support (~> 3.7.0)
124
+ rspec-mocks (3.7.0)
125
+ diff-lcs (>= 1.2.0, < 2.0)
126
+ rspec-support (~> 3.7.0)
127
+ rspec-rails (3.7.2)
128
+ actionpack (>= 3.0)
129
+ activesupport (>= 3.0)
130
+ railties (>= 3.0)
131
+ rspec-core (~> 3.7.0)
132
+ rspec-expectations (~> 3.7.0)
133
+ rspec-mocks (~> 3.7.0)
134
+ rspec-support (~> 3.7.0)
135
+ rspec-support (3.7.1)
136
+ ruby_dep (1.5.0)
137
+ shellany (0.0.1)
138
+ sqlite3 (1.3.13)
139
+ thor (0.20.0)
140
+ thread_safe (0.3.6)
141
+ tzinfo (1.2.5)
142
+ thread_safe (~> 0.1)
143
+
144
+ PLATFORMS
145
+ ruby
146
+
147
+ DEPENDENCIES
148
+ appraisal (~> 2.2.0)
149
+ bundler (~> 1.16)
150
+ byebug (~> 9.0)
151
+ combustion (~> 0.9.1)
152
+ database_cleaner (~> 1.7.0)
153
+ factory_bot_rails
154
+ faker (~> 1.9.1)
155
+ guard-rspec (~> 4.7.3)
156
+ quickery!
157
+ rake (~> 10.0)
158
+ rspec (~> 3.7.0)
159
+ rspec-rails (~> 3.7.2)
160
+ sqlite3 (~> 1.3.13)
161
+
162
+ BUNDLED WITH
163
+ 1.16.1
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: 'bundle exec appraisal rails-5 rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Jules Roman Polidario
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # Quickery
2
+
3
+ ## About
4
+
5
+ * Implements Law of Demeter by mapping associated record attributes as own attributes (one-way read-only)
6
+ * Consequently, speeds up SQL queries by removing joins queries between intermediary models, at the cost of slower writes.
7
+ * This is an anti-normalization pattern in favour of actual data-redundancy and faster queries. Use this only as necessary.
8
+
9
+ ## Dependencies
10
+
11
+ * **Rails 4 or 5**
12
+ * **(Rails 3 still untested)**
13
+
14
+ ## Setup
15
+ 1. Add the following to your `Gemfile`:
16
+
17
+ ```ruby
18
+ gem 'quickery', '~> 0.1'
19
+ ```
20
+
21
+ 2. Run:
22
+
23
+ ```bash
24
+ bundle install
25
+ ```
26
+
27
+ ## Usage Example 1
28
+
29
+ ```ruby
30
+ # app/models/employee.rb
31
+ class Employee < ApplicationRecord
32
+ # say we have the following attributes:
33
+ # branch_id:integer
34
+ # branch_company_name:string
35
+ belongs_to :branch
36
+
37
+ quickery do
38
+ # TL;RD: the following line means:
39
+ # make sure that this record's `branch_company_name` attribute will always have
40
+ # the same value as branch.company.name and updates the value accordingly if it changes
41
+ branch.company.name == :branch_company_name
42
+
43
+ # feel free to rename :branch_company_name as you wish; it's just like any other attribute anyway
44
+ # the == is a custom overloaded operator; it does not mean "is equal" but means "should equal to"
45
+ # branch.company.name is a fluid expression that defines the attribute dependency
46
+ # `branch` and `company` does not mean `branch` and `company` record
47
+
48
+ # you may add more inside this quickery-block: i.e:
49
+ # branch.name == :branch_name
50
+ # branch.id == :branch_id
51
+ # branch.company.country.name == :branch_company_country_name
52
+ end
53
+ end
54
+
55
+ # app/models/branch.rb
56
+ class Branch < ApplicationRecord
57
+ # say we have the following attributes:
58
+ # company_id:integer
59
+ belongs_to :company
60
+ end
61
+
62
+ # app/models/company.rb
63
+ class Company < ApplicationRecord
64
+ # say we have the following attributes:
65
+ # name:string
66
+ end
67
+ ```
68
+
69
+ ```bash
70
+ # bash
71
+ rails generate migration add_branch_company_name_to_employees branch_company_name:string
72
+ bundle exec rake db:migrate
73
+ ```
74
+
75
+ ```ruby
76
+ # rails console
77
+ company = Company.create!(name: 'Jollibee')
78
+ branch = Branch.create!(company: company)
79
+ employee = Employee.create!(branch: branch)
80
+
81
+ puts employee.branch_company_name
82
+ # => 'Jollibee'
83
+
84
+ # As you can see the `branch_company_name` attribute above has the same value as the associated record's attribute
85
+ # Now, let's try updating company, and you will see below that `branch_company_name` automatically gets updated as well
86
+
87
+ company.update!(name: 'Mang Inasal')
88
+
89
+ puts employee.branch_company_name
90
+ # => 'Jollibee'
91
+
92
+ # You need to reload the object, if you expect that it's been changed:
93
+ employee.reload
94
+
95
+ puts employee.branch_company_name
96
+ # => 'Mang Inasal'
97
+
98
+ # Now, let's try updating the intermediary association, and you will see below that `branch_company_name` would be updated accordingly
99
+ other_company = Company.create!(name: 'McDonalds')
100
+ branch.update!(company: other_company)
101
+
102
+ employee.reload
103
+
104
+ puts employee.branch_company_name
105
+ # => 'McDonalds'
106
+ ```
107
+
108
+ ## Usage Example 2
109
+
110
+ * let `Branch` and `Company` model be the same as the Usage Example 1 above
111
+
112
+ ```ruby
113
+ # app/models/employee.rb
114
+ class Employee < ApplicationRecord
115
+ belongs_to :branch
116
+ belongs_to :country, foreign_key: :branch_company_id
117
+
118
+ quickery do
119
+ branch.company.id == :branch_company_id
120
+ end
121
+ end
122
+ ```
123
+
124
+ ```bash
125
+ # bash
126
+ rails generate migration add_branch_company_id_to_employees branch_company_id:bigint
127
+ bundle exec rake db:migrate
128
+ ```
129
+
130
+ ```ruby
131
+ # rails console
132
+ company = Company.create!(name: 'Jollibee')
133
+ branch = Branch.create!(company: company)
134
+ employee = Employee.create!(branch: branch)
135
+
136
+ puts employee.branch_company_id
137
+ # => 1
138
+
139
+ puts Employee.where(company: company)
140
+ # => [#<Employee id: 1>]
141
+
142
+ # as you may notice, the query above is a lot simpler and faster instead of doing it normally like below (if not using Quickery)
143
+ # you may however still using belongs_to `:through` to achieve the simplified query like above, but it's still a lot slower because of JOINS
144
+ puts Employee.joins(branch: :company).where(companies: { id: company.id })
145
+ # => [#<Employee id: 1>]
146
+ ```
147
+
148
+ ## DSL
149
+
150
+ ### For any subclass of `ActiveRecord::Base`:
151
+
152
+ #### Class Methods:
153
+
154
+ ##### `quickery(&block)`
155
+ * returns a `Quickery::AssociationBuilder` object
156
+ * block is executed in the context of the `Quickery::AssociationBuilder` object,
157
+ which means that you cannot access the model instance inside the block, as you are not supposed to.
158
+ * inside the block you may define "quickery-defined attribute mappings";
159
+ each mapping will create a `Quickery::QuickeryBuilder` object. i.e:
160
+ * `branch.company.country.category.name == :branch_company_country_category_name`
161
+ * You are required to specify `belongs_to :branch` association in this model.
162
+ * Similarly, you are required to specify `belongs_to :company` inside `Branch` model, `belongs_to :country` inside `Company` model; etc...
163
+ * each `Quickery::AssociationBuilder` defines a set of "hidden" `before_save`, `before_update`, `before_destroy`, and `before_create` callbacks across all models specified in the quickery-defined attribute association chain.
164
+ * quickery-defined attributes such as say `:branch_company_country_category_name` are updated by Quickery automatically whenever any of it's dependent records across models have been changed. Note that updates in this way do not trigger model callbacks, as I wanted to isolate logic and scope of Quickery by not triggering model callbacks that you already have.
165
+ * quickery-defined attributes such as say `:branch_company_country_category_name` are READ-only! Do not update these attributes manually. You can, but it will not automatically update the other end, and thus will break data integrity. If you want to re-update these attributes to match the other end, see `recreate_quickery_cache!` below.
166
+
167
+ ##### `quickery_builders`
168
+ * returns an `Array` of `Quickery::QuickeryBuilder` objects that have already been defined
169
+ * for more info, see `quickery(&block)` above
170
+ * you normally do not need to use this method
171
+
172
+ #### Instance Methods:
173
+
174
+ ##### `recreate_quickery_cache!`
175
+ * force-updates the quickery-defined attributes
176
+ * useful if you already have records, and you want these old records to be updated immediately
177
+ * i.e. you can do so something like the following:
178
+ ```ruby
179
+ Employee.each do |employee|
180
+ employee.recreate_quickery_cache!
181
+ end
182
+ ```
183
+
184
+ ##### `determine_quickery_value(depender_column_name)`
185
+ * returns the current "actual" supposed value of the "original" dependee column
186
+ * useful for debugging to check if the quickery-defined attributes do not have correct mapped values
187
+ * i.e. you can do something like the following:
188
+
189
+ ```ruby
190
+ employee = Employee.first
191
+ puts employee.determine_quickery_value(:branch_company_country_name)
192
+ # => 'Ireland'
193
+ ```
194
+
195
+ ## TODOs
196
+ * Possibly support two-way mapping of attributes? So that you can do, say... `employee.update!(branch_company_name: 'somenewcompanyname')`
197
+
198
+ ## Contributing
199
+ * pull requests and forks are very much welcomed! :) Let me know if you find any bug! Thanks
200
+
201
+ ## License
202
+ * MIT
203
+
204
+ ## Developer Guide
205
+ * see [developer_guide.md](developer_guide.md)
206
+
207
+ ## Changelog
208
+ * 0.1.0
209
+ * initial beta release
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "quickery"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :all
9
+ run Combustion::Application
@@ -0,0 +1,26 @@
1
+ ## Development
2
+
3
+ ### Publishing as Ruby Gem
4
+
5
+ ```bash
6
+ # [increment gem VERSION]
7
+ gem build quickery
8
+ gem push quickery-X.X.X.gem
9
+ ```
10
+
11
+ ## Test
12
+
13
+ * Test suite makes use of the following gems worth mentioning (in addition to some others):
14
+ * [rspec-rails](https://github.com/rspec/rspec-rails)
15
+ * [combustion](https://github.com/pat/combustion)
16
+ * [appraisal](https://github.com/thoughtbot/appraisal)
17
+
18
+ ```bash
19
+ # to auto-test specs whenever a spec file has been modified:
20
+ bundle exec guard
21
+
22
+ # to manually run specs for a particular rails version (for more info: see Appraisals file):
23
+ bundle exec appraisal rails-5 rspec
24
+ # or
25
+ bundle exec appraisal rails-4 rspec
26
+ ```
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "3.2.22.5"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "4.2.10"
6
+
7
+ group :development, :test do
8
+ gem "factory_bot_rails", require: false
9
+ end
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "5.2.1"
6
+
7
+ group :development, :test do
8
+ gem "factory_bot_rails", require: false
9
+ end
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,44 @@
1
+ require 'byebug'
2
+ require 'active_support'
3
+
4
+ module Quickery
5
+ module ActiveRecordExtensions
6
+ module DSL
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.include InstanceMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def quickery(&block)
14
+ association_builder = AssociationBuilder.new(model: self)
15
+ association_builder.instance_exec(&block)
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ def recreate_quickery_cache!
21
+ self.class.quickery_builders.each do |depender_column_name, quickery_builder|
22
+ new_value = determine_quickery_value(depender_column_name)
23
+ update_columns(depender_column_name => new_value)
24
+ end
25
+
26
+ true
27
+ end
28
+
29
+ def determine_quickery_value(depender_column_name)
30
+ quickery_builder = self.class.quickery_builders[depender_column_name]
31
+
32
+ raise ArgumentError, "No defined quickery builder for #{depender_column_name}. Defined values are #{self.class.quickery_builders.keys}" unless quickery_builder
33
+
34
+ dependee_record = quickery_builder.first_association_builder._quickery_dependee_record(self)
35
+ dependee_record.send(quickery_builder.dependee_column_name)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ ActiveSupport.on_load(:active_record) do
43
+ include Quickery::ActiveRecordExtensions::DSL
44
+ end
@@ -0,0 +1,108 @@
1
+ module Quickery
2
+ class AssociationBuilder
3
+ attr_reader :model
4
+ attr_reader :parent_builder
5
+ attr_reader :child_builder
6
+ attr_reader :inverse_association_name
7
+ attr_reader :belongs_to
8
+
9
+ def initialize(model:, parent_builder: nil, inverse_association_name: nil)
10
+ @model = model
11
+ @parent_builder = parent_builder
12
+ @inverse_association_name = inverse_association_name
13
+ @reflections = model.reflections
14
+ @belongs_to_association_names = @reflections.map{ |key, value| value.macro == :belongs_to ? key : nil }.compact
15
+ @column_names = model.column_names
16
+ end
17
+
18
+ # we need to prepend _quickery to all methods, to make sure no conflicts with association names that are dynamically invoked through `method_missing` below
19
+
20
+ def _quickery_get_child_builders(include_self: false, builders: [])
21
+ builders << self if include_self
22
+
23
+ if @child_builder.nil?
24
+ builders
25
+ else
26
+ builders << @child_builder
27
+ return @child_builder._quickery_get_child_builders(builders: builders)
28
+ end
29
+ end
30
+
31
+ def _quickery_get_parent_builders(include_self: false, builders: [])
32
+ builders << self if include_self
33
+
34
+ if @parent_builder.nil?
35
+ builders
36
+ else
37
+ builders << @parent_builder
38
+ @parent_builder._quickery_get_parent_builders(builders: builders)
39
+ end
40
+ end
41
+
42
+ def _quickery_get_joins_arg(current_joins_arg = nil)
43
+ if @parent_builder.nil?
44
+ current_joins_arg
45
+ else
46
+ if current_joins_arg.nil?
47
+ @parent_builder._quickery_get_joins_arg(@inverse_association_name.to_sym)
48
+ else
49
+ @parent_builder._quickery_get_joins_arg({ @inverse_association_name.to_sym => current_joins_arg })
50
+ end
51
+ end
52
+ end
53
+
54
+ def _quickery_dependent_records(record_to_be_saved)
55
+ primary_key_value = record_to_be_saved.send(record_to_be_saved.class.primary_key)
56
+ most_parent_model = _quickery_get_parent_builders.last.model
57
+
58
+ records = most_parent_model.all
59
+
60
+ unless (joins_arg = _quickery_get_joins_arg).nil?
61
+ records = records.joins(joins_arg)
62
+ end
63
+
64
+ records = records.where(
65
+ model.table_name => {
66
+ model.primary_key => primary_key_value
67
+ }
68
+ )
69
+ end
70
+
71
+ def _quickery_dependee_record(record_to_be_saved)
72
+ raise ArgumentError, 'argument should be an instance of @model' unless record_to_be_saved.is_a? model
73
+
74
+ _quickery_get_child_builders(include_self: true).inject(record_to_be_saved) do |record, association_builder|
75
+ if association_builder.belongs_to
76
+ record.send(association_builder.belongs_to.name)
77
+ else
78
+ record
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def method_missing(method_name, *args, &block)
86
+ method_name_str = method_name.to_s
87
+ if @belongs_to_association_names.include? method_name_str
88
+ @belongs_to = @reflections[method_name_str]
89
+ @child_builder = AssociationBuilder.new(model: belongs_to.class_name.constantize, parent_builder: self, inverse_association_name: method_name_str)
90
+ elsif @column_names.include? method_name_str
91
+ QuickeryBuilder.new(dependee_column_name: method_name_str, last_association_builder: self)
92
+ else
93
+ super
94
+ end
95
+ end
96
+
97
+ def respond_to_missing(method_name, include_private = false)
98
+ method_name_str = method_name.to_s
99
+ if @belongs_to_association_names.include? method_name_str
100
+ true
101
+ elsif @column_names.include? method_name_str
102
+ true
103
+ else
104
+ super
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,107 @@
1
+ module Quickery
2
+ class CallbacksBuilder
3
+ attr_reader :quickery_builder
4
+
5
+ def initialize(quickery_builder:, should_add_callbacks: true)
6
+ @quickery_builder = quickery_builder
7
+ add_callbacks if should_add_callbacks
8
+ end
9
+
10
+ private
11
+
12
+ def add_callbacks
13
+ add_callback_to_depender_model
14
+ add_callback_to_dependee_model
15
+ add_callback_to_each_intermediate_model
16
+ end
17
+
18
+ # add callback to immediately sync value after a record has been created / updated
19
+ def add_callback_to_depender_model
20
+ first_association_builder = @quickery_builder.first_association_builder
21
+ depender_column_name = @quickery_builder.depender_column_name
22
+ dependee_column_name = @quickery_builder.dependee_column_name
23
+
24
+ first_association_builder.model.class_exec do
25
+
26
+ # before create or update
27
+ before_save do
28
+ if changes.keys.include? first_association_builder.belongs_to.foreign_key
29
+ if send(first_association_builder.belongs_to.foreign_key).nil?
30
+ new_value = nil
31
+ else
32
+ dependee_record = first_association_builder._quickery_dependee_record(self)
33
+ new_value = dependee_record.send(dependee_column_name)
34
+ end
35
+
36
+ assign_attributes(depender_column_name => new_value)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # add callback to sync changes when dependee_column has been updated
43
+ def add_callback_to_dependee_model
44
+ last_association_builder = @quickery_builder.last_association_builder
45
+ depender_column_name = @quickery_builder.depender_column_name
46
+ dependee_column_name = @quickery_builder.dependee_column_name
47
+
48
+ last_association_builder.model.class_exec do
49
+
50
+ before_update do
51
+ if changes.keys.include? dependee_column_name
52
+ new_value = send(dependee_column_name)
53
+
54
+ dependent_records = last_association_builder._quickery_dependent_records(self)
55
+ dependent_records.update_all(depender_column_name => new_value)
56
+ end
57
+ end
58
+
59
+ before_destroy do
60
+ if attributes.keys.include? dependee_column_name
61
+ new_value = nil
62
+
63
+ dependent_records = last_association_builder._quickery_dependent_records(self)
64
+ dependent_records.update_all(depender_column_name => new_value)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # also add callbacks to sync changes when intermediary associations have been changed (this does not include first and last builder)
71
+ def add_callback_to_each_intermediate_model
72
+ last_association_builder = @quickery_builder.last_association_builder
73
+ depender_column_name = @quickery_builder.depender_column_name
74
+ dependee_column_name = @quickery_builder.dependee_column_name
75
+
76
+ last_association_builder._quickery_get_parent_builders(include_self: true)[1..-2].each do |association_builder|
77
+ intermediate_model = association_builder.model
78
+
79
+ intermediate_model.class_exec do
80
+
81
+ before_update do
82
+ if changes.keys.include? association_builder.belongs_to.foreign_key
83
+ if send(association_builder.belongs_to.foreign_key).nil?
84
+ new_value = nil
85
+ else
86
+ dependee_record = association_builder._quickery_dependee_record(self)
87
+ new_value = dependee_record.send(dependee_column_name)
88
+ end
89
+
90
+ dependent_records = association_builder._quickery_dependent_records(self)
91
+ dependent_records.update_all(depender_column_name => new_value)
92
+ end
93
+ end
94
+
95
+ before_destroy do
96
+ if attributes.keys.include? association_builder.belongs_to.foreign_key
97
+ new_value = nil
98
+
99
+ dependent_records = last_association_builder._quickery_dependent_records(self)
100
+ dependent_records.update_all(depender_column_name => new_value)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,41 @@
1
+ module Quickery
2
+ class QuickeryBuilder
3
+ attr_reader :model
4
+ attr_reader :depender_column_name
5
+ attr_reader :dependee_column_name
6
+ attr_reader :first_association_builder
7
+ attr_reader :last_association_builder
8
+ attr_reader :callbacks_builder
9
+
10
+ def initialize(dependee_column_name:, last_association_builder:)
11
+ @dependee_column_name = dependee_column_name
12
+ @last_association_builder = last_association_builder
13
+ @first_association_builder = last_association_builder._quickery_get_parent_builders.last
14
+ @model = @first_association_builder.model
15
+ end
16
+
17
+ def ==(depender_column_name)
18
+ @depender_column_name = depender_column_name
19
+
20
+ @callbacks_builder = CallbacksBuilder.new(quickery_builder: self)
21
+
22
+ define_quickery_builders_in_model_class unless @model.respond_to? :quickery_builders
23
+
24
+ # include this to the list of quickery builders defined for this model
25
+ @model.quickery_builders[depender_column_name] = self
26
+ end
27
+
28
+ private
29
+
30
+ def define_quickery_builders_in_model_class
31
+ # set default empty Hash if first time setting quickery_builders
32
+ @model.class_eval do
33
+ @quickery_builders = {}
34
+
35
+ class << self
36
+ attr_reader :quickery_builders
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module Quickery
2
+ VERSION = '0.1.0'
3
+ end
data/lib/quickery.rb ADDED
@@ -0,0 +1,6 @@
1
+ Dir[__dir__ + '/quickery/*.rb'].each {|file| require file }
2
+ Dir[__dir__ + '/quickery/active_record_extensions/*.rb'].each {|file| require file }
3
+
4
+ module Quickery
5
+ # Your code goes here...
6
+ end
data/quickery.gemspec ADDED
@@ -0,0 +1,38 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'quickery/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'quickery'
8
+ spec.version = Quickery::VERSION
9
+ spec.authors = ['Jules Roman Polidario']
10
+ spec.email = ['jrpolidario@gmail.com']
11
+
12
+ spec.summary = 'Database Anti-normalization pattern implementing Law of Demeter by mapping associated record attributes as own attributes (one-way read-only), and therefore improves query speeds at the cost of slower writes.'
13
+ spec.description = 'Implements Law of Demeter by mapping associated record attributes as own attributes (one-way read-only). Consequently, speeds up SQL queries by removing joins queries between intermediary models, at the cost of slower writes. This is an anti-normalization pattern in favour of actual data-redundancy and faster queries. Use this only as necessary.'
14
+ spec.homepage = 'https://github.com/jrpolidario/quickery'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'activerecord', '~> 4.0'
25
+ spec.add_dependency 'activesupport', '~> 4.0'
26
+
27
+ spec.add_development_dependency 'byebug', '~> 9.0'
28
+ spec.add_development_dependency 'bundler', '~> 1.16'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'rspec', '~> 3.7.0'
31
+ spec.add_development_dependency 'rspec-rails', '~> 3.7.2'
32
+ spec.add_development_dependency 'sqlite3', '~> 1.3.13'
33
+ spec.add_development_dependency 'database_cleaner', '~> 1.7.0'
34
+ spec.add_development_dependency 'faker', '~> 1.9.1'
35
+ spec.add_development_dependency 'combustion', '~> 0.9.1'
36
+ spec.add_development_dependency 'guard-rspec', '~> 4.7.3'
37
+ spec.add_development_dependency 'appraisal', '~> 2.2.0'
38
+ end
metadata ADDED
@@ -0,0 +1,257 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quickery
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jules Roman Polidario
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-09-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '9.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '9.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.16'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.16'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 3.7.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 3.7.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec-rails
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 3.7.2
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 3.7.2
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 1.3.13
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 1.3.13
125
+ - !ruby/object:Gem::Dependency
126
+ name: database_cleaner
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.7.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.7.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: faker
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 1.9.1
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 1.9.1
153
+ - !ruby/object:Gem::Dependency
154
+ name: combustion
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 0.9.1
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 0.9.1
167
+ - !ruby/object:Gem::Dependency
168
+ name: guard-rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 4.7.3
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 4.7.3
181
+ - !ruby/object:Gem::Dependency
182
+ name: appraisal
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: 2.2.0
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: 2.2.0
195
+ description: Implements Law of Demeter by mapping associated record attributes as
196
+ own attributes (one-way read-only). Consequently, speeds up SQL queries by removing
197
+ joins queries between intermediary models, at the cost of slower writes. This is
198
+ an anti-normalization pattern in favour of actual data-redundancy and faster queries.
199
+ Use this only as necessary.
200
+ email:
201
+ - jrpolidario@gmail.com
202
+ executables: []
203
+ extensions: []
204
+ extra_rdoc_files: []
205
+ files:
206
+ - ".gitignore"
207
+ - ".rspec"
208
+ - ".travis.yml"
209
+ - Appraisals
210
+ - Gemfile
211
+ - Gemfile.lock
212
+ - Guardfile
213
+ - LICENSE.txt
214
+ - README.md
215
+ - Rakefile
216
+ - bin/console
217
+ - bin/setup
218
+ - config.ru
219
+ - developer_guide.md
220
+ - gemfiles/.bundle/config
221
+ - gemfiles/rails_3.gemfile
222
+ - gemfiles/rails_4.gemfile
223
+ - gemfiles/rails_5.gemfile
224
+ - lib/quickery.rb
225
+ - lib/quickery/active_record_extensions/dsl.rb
226
+ - lib/quickery/association_builder.rb
227
+ - lib/quickery/callbacks_builder.rb
228
+ - lib/quickery/quickery_builder.rb
229
+ - lib/quickery/version.rb
230
+ - quickery.gemspec
231
+ homepage: https://github.com/jrpolidario/quickery
232
+ licenses:
233
+ - MIT
234
+ metadata: {}
235
+ post_install_message:
236
+ rdoc_options: []
237
+ require_paths:
238
+ - lib
239
+ required_ruby_version: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ required_rubygems_version: !ruby/object:Gem::Requirement
245
+ requirements:
246
+ - - ">="
247
+ - !ruby/object:Gem::Version
248
+ version: '0'
249
+ requirements: []
250
+ rubyforge_project:
251
+ rubygems_version: 2.7.6
252
+ signing_key:
253
+ specification_version: 4
254
+ summary: Database Anti-normalization pattern implementing Law of Demeter by mapping
255
+ associated record attributes as own attributes (one-way read-only), and therefore
256
+ improves query speeds at the cost of slower writes.
257
+ test_files: []