quickery 0.1.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +68 -33
- data/lib/quickery.rb +1 -0
- data/lib/quickery/active_record_extensions.rb +16 -0
- data/lib/quickery/active_record_extensions/callbacks.rb +175 -0
- data/lib/quickery/active_record_extensions/dsl.rb +13 -15
- data/lib/quickery/association_chain.rb +106 -0
- data/lib/quickery/callbacks_builder.rb +20 -87
- data/lib/quickery/errors/invalid_association_or_attribute_error.rb +10 -0
- data/lib/quickery/mappings_builder.rb +38 -0
- data/lib/quickery/quickery_builder.rb +17 -13
- data/lib/quickery/version.rb +1 -1
- data/other_similar_gems_comparison.md +2 -0
- metadata +7 -3
- data/lib/quickery/association_builder.rb +0 -108
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 20c5125563b48a48ba73907330199cf2b7d330e0e78bc3449db1ba92f29783aa
|
4
|
+
data.tar.gz: 047cc01e20747481b825ca1d4f26dfe34ba4ab31429a005be4b8767cf51b6e12
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cdc80db27ccdbe249a2592bc0e537cd0cf4a8c2146afcc0a306ffadd01cb7a76e1181492d6e44795bd94862c23d7e1aadbaf42e19962f99f1ee4b2985ebdb21
|
7
|
+
data.tar.gz: 72f951760cd8737b117d1f11865cb2cc4c6b4ba5c427d86dcdffa704a2025d51432d45c69b0226eb0b2c42c20dd241ab93909dc8a83d8b4d51e8113db4eaeed8
|
data/README.md
CHANGED
@@ -17,7 +17,7 @@
|
|
17
17
|
1. Add the following to your `Gemfile`:
|
18
18
|
|
19
19
|
```ruby
|
20
|
-
gem 'quickery', '~> 0
|
20
|
+
gem 'quickery', '~> 1.0'
|
21
21
|
```
|
22
22
|
|
23
23
|
2. Run:
|
@@ -37,22 +37,13 @@ class Employee < ApplicationRecord
|
|
37
37
|
|
38
38
|
belongs_to :branch
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
# the == is a custom overloaded operator; it does not mean "is equal" but means "should equal to"
|
48
|
-
# branch.company.name is a fluid expression that defines the attribute dependency
|
49
|
-
# `branch` and `company` does not mean `branch` and `company` record
|
50
|
-
|
51
|
-
# you may add more inside this quickery-block: i.e:
|
52
|
-
# branch.name == :branch_name
|
53
|
-
# branch.id == :branch_id
|
54
|
-
# branch.company.country.name == :branch_company_country_name
|
55
|
-
end
|
40
|
+
# TL;DR: the following line means:
|
41
|
+
# make sure that this record's `branch_company_name` attribute will always have the same value as
|
42
|
+
# branch.company.name and auto-updates the value if it (or any associated record in between) changes
|
43
|
+
|
44
|
+
quickery branch: { company: { name: :branch_company_name } }
|
45
|
+
|
46
|
+
# feel free to rename :branch_company_name as you wish; it's just like any other attribute anyway
|
56
47
|
end
|
57
48
|
|
58
49
|
# app/models/branch.rb
|
@@ -126,17 +117,15 @@ end
|
|
126
117
|
# app/models/employee.rb
|
127
118
|
class Employee < ApplicationRecord
|
128
119
|
belongs_to :branch
|
129
|
-
belongs_to :company, foreign_key: :branch_company_id
|
120
|
+
belongs_to :company, foreign_key: :branch_company_id, optional: true
|
130
121
|
|
131
|
-
quickery
|
132
|
-
branch.company.id == :branch_company_id
|
133
|
-
end
|
122
|
+
quickery { branch: { company: { id: :branch_company_id } } }
|
134
123
|
end
|
135
124
|
```
|
136
125
|
|
137
126
|
```bash
|
138
127
|
# bash
|
139
|
-
rails generate migration add_branch_company_id_to_employees branch_company_id:bigint
|
128
|
+
rails generate migration add_branch_company_id_to_employees branch_company_id:bigint:index
|
140
129
|
bundle exec rake db:migrate
|
141
130
|
```
|
142
131
|
|
@@ -149,31 +138,72 @@ employee = Employee.create!(branch: branch)
|
|
149
138
|
puts employee.branch_company_id
|
150
139
|
# => 1
|
151
140
|
|
141
|
+
puts employee.company
|
142
|
+
# => #<Company id: 1 name: 'Jollibee'>
|
143
|
+
|
152
144
|
puts Employee.where(company: company)
|
153
145
|
# => [#<Employee id: 1>]
|
154
146
|
|
155
147
|
# as you may notice, the query above is a lot simpler and faster instead of doing it normally like below (if not using Quickery)
|
156
|
-
# you may however still use
|
148
|
+
# you may however still use `has_many :through` to achieve a simplified code: `company.employees`, but it's still a lot slower because of JOINS
|
157
149
|
puts Employee.joins(branch: :company).where(companies: { id: company.id })
|
158
150
|
# => [#<Employee id: 1>]
|
159
151
|
```
|
160
152
|
|
153
|
+
## Other Usage Examples
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
# app/models/employee.rb
|
157
|
+
class Employee < ApplicationRecord
|
158
|
+
# multiple-attributes and/or multiple-associations; as many, and as deep as you wish
|
159
|
+
quickery(
|
160
|
+
branch: {
|
161
|
+
name: :branch_name,
|
162
|
+
address: :branch_address,
|
163
|
+
company: {
|
164
|
+
name: :branch_company_name
|
165
|
+
}
|
166
|
+
},
|
167
|
+
user: {
|
168
|
+
first_name: :user_first_name,
|
169
|
+
last_name: :user_last_name
|
170
|
+
}
|
171
|
+
)
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
# app/models/employee.rb
|
177
|
+
class Employee < ApplicationRecord
|
178
|
+
# `quickery` can be called multiple times
|
179
|
+
quickery { branch: { name: :branch_name } }
|
180
|
+
quickery { branch: { address: :branch_address } }
|
181
|
+
quickery { branch: { company: { name: :branch_company_name } } }
|
182
|
+
quickery { user: { first_name: :user_first_name } }
|
183
|
+
quickery { user: { last_name: :user_last_name } }
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
## Gotchas
|
188
|
+
* Quickery makes use of Rails model callbacks such as `before_save`. This meant that data-integrity holds unless `update_columns` or `update_column` is used which bypasses model callbacks, or unless any manual SQL update is performed.
|
189
|
+
* Quickery does not automatically update old records existing in the database that were created before you integrate Quickery, or before you add new/more Quickery-attributes for that model. One solution is [`recreate_quickery_cache!`](#recreate_quickery_cache) below.
|
190
|
+
|
161
191
|
## DSL
|
162
192
|
|
163
193
|
### For any subclass of `ActiveRecord::Base`:
|
164
194
|
|
195
|
+
* defines a set of "hidden" Quickery `before_create`, `before_update`, and `before_destroy` callbacks needed by Quickery to perform the "syncing" of attribute values
|
196
|
+
|
165
197
|
#### Class Methods:
|
166
198
|
|
167
|
-
##### `quickery(
|
168
|
-
*
|
169
|
-
*
|
170
|
-
|
171
|
-
*
|
172
|
-
|
173
|
-
* `branch.company.country.category.name == :branch_company_country_category_name`
|
174
|
-
* You are required to specify `belongs_to :branch` association in this model.
|
199
|
+
##### `quickery(mappings)`
|
200
|
+
* mappings (Hash)
|
201
|
+
* each mapping will create a `Quickery::QuickeryBuilder` object. i.e:
|
202
|
+
* `{ branch: { name: :branch_name }` will create one `Quickery::QuickeryBuilder`, while
|
203
|
+
* `{ branch: { name: :branch_name, id: :branch_id }` will create two `Quickery::QuickeryBuilder`
|
204
|
+
* In this particular example, you are required to specify `belongs_to :branch` in this model
|
175
205
|
* Similarly, you are required to specify `belongs_to :company` inside `Branch` model, `belongs_to :country` inside `Company` model; etc...
|
176
|
-
*
|
206
|
+
* defines a set of "hidden" Quickery `before_save`, `before_update`, `before_destroy`, and `before_create` callbacks across all models specified in the quickery-defined attribute association chain.
|
177
207
|
* 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.
|
178
208
|
* 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.
|
179
209
|
|
@@ -207,7 +237,9 @@ puts Employee.joins(branch: :company).where(companies: { id: company.id })
|
|
207
237
|
|
208
238
|
## TODOs
|
209
239
|
* Possibly support two-way mapping of attributes? So that you can do, say... `employee.update!(branch_company_name: 'somenewcompanyname')`
|
210
|
-
*
|
240
|
+
* Support `has_many` as currently only `belongs_to` is supported. This would then allow us to cache Array of values.
|
241
|
+
* Support custom-methods-values like [`persistize`](https://github.com/bebanjo/persistize), if it's easy enough to integrate something similar
|
242
|
+
* Support background-processing like in [`flattery`](https://github.com/evendis/flattery)
|
211
243
|
|
212
244
|
## Other Similar Gems
|
213
245
|
See [my detailed comparisons](other_similar_gems_comparison.md)
|
@@ -232,6 +264,9 @@ See [my detailed comparisons](other_similar_gems_comparison.md)
|
|
232
264
|
5. Create new Pull Request
|
233
265
|
|
234
266
|
## Changelog
|
267
|
+
* 1.0.0
|
268
|
+
* Done (TODO): DSL changed from quickery (block) into quickery (hash). Thanks to @xire28 and @sshaw_ in my [reddit post](https://www.reddit.com/r/ruby/comments/9dlcc5/i_just_published_a_new_gem_quickery_an/) for the suggestion.
|
269
|
+
* Done (TODO): Now updates in one go, instead of updating record per quickery-attribute, thereby greatly improving speed.
|
235
270
|
* 0.1.4
|
236
271
|
* add `railstie` as dependency to fix undefined constant error
|
237
272
|
* 0.1.3
|
data/lib/quickery.rb
CHANGED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module Quickery
|
4
|
+
module ActiveRecordExtensions
|
5
|
+
class << self
|
6
|
+
def included(base)
|
7
|
+
base.include Quickery::ActiveRecordExtensions::DSL
|
8
|
+
base.include Quickery::ActiveRecordExtensions::Callbacks
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveSupport.on_load(:active_record) do
|
15
|
+
include Quickery::ActiveRecordExtensions
|
16
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Quickery
|
2
|
+
module ActiveRecordExtensions
|
3
|
+
module Callbacks
|
4
|
+
class << self
|
5
|
+
def included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.include InstanceMethods
|
8
|
+
base.class_eval do
|
9
|
+
before_create :quickery_before_create_callback
|
10
|
+
before_update :quickery_before_update_callback
|
11
|
+
before_destroy :quickery_before_destroy_callback
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
private
|
21
|
+
|
22
|
+
def quickery_before_create_callback
|
23
|
+
model = self.class
|
24
|
+
changed_attributes = changes.keys
|
25
|
+
|
26
|
+
if model.quickery_association_chain_dependers.present?
|
27
|
+
model.quickery_association_chain_dependers.each do |association_chain_depender|
|
28
|
+
quickery_builder = association_chain_depender.quickery_builder
|
29
|
+
depender_column_name = quickery_builder.depender_column_name
|
30
|
+
dependee_column_name = quickery_builder.dependee_column_name
|
31
|
+
|
32
|
+
if changed_attributes.include? association_chain_depender.belongs_to.foreign_key
|
33
|
+
if send(association_chain_depender.belongs_to.foreign_key).nil?
|
34
|
+
new_value = nil
|
35
|
+
else
|
36
|
+
dependee_record = association_chain_depender.dependee_record(self)
|
37
|
+
new_value = dependee_record.send(dependee_column_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
assign_attributes(depender_column_name => new_value)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def quickery_before_update_callback
|
47
|
+
model = self.class
|
48
|
+
changed_attributes = changes.keys
|
49
|
+
|
50
|
+
if model.quickery_association_chain_dependers.present?
|
51
|
+
model.quickery_association_chain_dependers.each do |association_chain_depender|
|
52
|
+
quickery_builder = association_chain_depender.quickery_builder
|
53
|
+
depender_column_name = quickery_builder.depender_column_name
|
54
|
+
dependee_column_name = quickery_builder.dependee_column_name
|
55
|
+
|
56
|
+
if changed_attributes.include? association_chain_depender.belongs_to.foreign_key
|
57
|
+
if send(association_chain_depender.belongs_to.foreign_key).nil?
|
58
|
+
new_value = nil
|
59
|
+
else
|
60
|
+
dependee_record = association_chain_depender.dependee_record(self)
|
61
|
+
new_value = dependee_record.send(dependee_column_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
assign_attributes(depender_column_name => new_value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
dependent_records_attributes_to_be_updated = {}
|
70
|
+
|
71
|
+
if model.quickery_association_chain_dependees.present?
|
72
|
+
model.quickery_association_chain_dependees.each do |association_chain_dependee|
|
73
|
+
quickery_builder = association_chain_dependee.quickery_builder
|
74
|
+
depender_column_name = quickery_builder.depender_column_name
|
75
|
+
dependee_column_name = quickery_builder.dependee_column_name
|
76
|
+
|
77
|
+
if changed_attributes.include? dependee_column_name
|
78
|
+
new_value = send(dependee_column_name)
|
79
|
+
|
80
|
+
dependent_records = association_chain_dependee.dependent_records(self)
|
81
|
+
# use the SQL as the uniqueness identifier, so that multiple quickery-attributes dependent-records are updated in one go, instead of updating each
|
82
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym] ||= {}
|
83
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:dependent_records] ||= dependent_records
|
84
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values] ||= {}
|
85
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values][depender_column_name.to_sym] = new_value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if model.quickery_association_chain_intermediaries.present?
|
91
|
+
model.quickery_association_chain_intermediaries.each do |association_chain_intermediary|
|
92
|
+
quickery_builder = association_chain_intermediary.quickery_builder
|
93
|
+
depender_column_name = quickery_builder.depender_column_name
|
94
|
+
dependee_column_name = quickery_builder.dependee_column_name
|
95
|
+
|
96
|
+
if changed_attributes.include? association_chain_intermediary.belongs_to.foreign_key
|
97
|
+
if send(association_chain_intermediary.belongs_to.foreign_key).nil?
|
98
|
+
new_value = nil
|
99
|
+
else
|
100
|
+
dependee_record = association_chain_intermediary.dependee_record(self)
|
101
|
+
new_value = dependee_record.send(dependee_column_name)
|
102
|
+
end
|
103
|
+
|
104
|
+
dependent_records = association_chain_intermediary.dependent_records(self)
|
105
|
+
# use the SQL as the uniqueness identifier, so that multiple quickery-attributes dependent-records are updated in one go, instead of updating each
|
106
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym] ||= {}
|
107
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:dependent_records] ||= dependent_records
|
108
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values] ||= {}
|
109
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values][depender_column_name.to_sym] = new_value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
dependent_records_attributes_to_be_updated.each do |sql_identifier, hash|
|
115
|
+
dependent_records = hash.fetch(:dependent_records)
|
116
|
+
new_values = hash.fetch(:new_values)
|
117
|
+
|
118
|
+
dependent_records.update_all(new_values)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def quickery_before_destroy_callback
|
123
|
+
model = self.class
|
124
|
+
|
125
|
+
dependent_records_attributes_to_be_updated = {}
|
126
|
+
|
127
|
+
if model.quickery_association_chain_dependees.present?
|
128
|
+
model.quickery_association_chain_dependees.each do |association_chain_dependee|
|
129
|
+
quickery_builder = association_chain_dependee.quickery_builder
|
130
|
+
depender_column_name = quickery_builder.depender_column_name
|
131
|
+
dependee_column_name = quickery_builder.dependee_column_name
|
132
|
+
|
133
|
+
if attributes.keys.include? dependee_column_name
|
134
|
+
new_value = nil
|
135
|
+
|
136
|
+
dependent_records = association_chain_dependee.dependent_records(self)
|
137
|
+
# use the SQL as the uniqueness identifier, so that multiple quickery-attributes dependent-records are updated in one go, instead of updating each
|
138
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym] ||= {}
|
139
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:dependent_records] ||= dependent_records
|
140
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values] ||= {}
|
141
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values][depender_column_name.to_sym] = new_value
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
if model.quickery_association_chain_intermediaries.present?
|
147
|
+
model.quickery_association_chain_intermediaries.each do |association_chain_intermediary|
|
148
|
+
quickery_builder = association_chain_intermediary.quickery_builder
|
149
|
+
depender_column_name = quickery_builder.depender_column_name
|
150
|
+
dependee_column_name = quickery_builder.dependee_column_name
|
151
|
+
|
152
|
+
if attributes.keys.include? association_chain_intermediary.belongs_to.foreign_key
|
153
|
+
new_value = nil
|
154
|
+
|
155
|
+
dependent_records = association_chain_intermediary.dependent_records(self)
|
156
|
+
# use the SQL as the uniqueness identifier, so that multiple quickery-attributes dependent-records are updated in one go, instead of updating each
|
157
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym] ||= {}
|
158
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:dependent_records] ||= dependent_records
|
159
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values] ||= {}
|
160
|
+
dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values][depender_column_name.to_sym] = new_value
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
dependent_records_attributes_to_be_updated.each do |sql_identifier, hash|
|
166
|
+
dependent_records = hash.fetch(:dependent_records)
|
167
|
+
new_values = hash.fetch(:new_values)
|
168
|
+
|
169
|
+
dependent_records.update_all(new_values)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -1,17 +1,21 @@
|
|
1
|
-
require 'active_support'
|
2
|
-
|
3
1
|
module Quickery
|
4
2
|
module ActiveRecordExtensions
|
5
3
|
module DSL
|
6
|
-
|
7
|
-
base
|
8
|
-
|
4
|
+
class << self
|
5
|
+
def included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.include InstanceMethods
|
8
|
+
end
|
9
9
|
end
|
10
10
|
|
11
11
|
module ClassMethods
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
attr_accessor :quickery_association_chain_dependers
|
13
|
+
attr_accessor :quickery_association_chain_dependees
|
14
|
+
attr_accessor :quickery_association_chain_intermediaries
|
15
|
+
|
16
|
+
def quickery(mappings)
|
17
|
+
mappings_builder = MappingsBuilder.new(model: self, mappings: mappings.with_indifferent_access)
|
18
|
+
mappings_builder.map_attributes
|
15
19
|
end
|
16
20
|
end
|
17
21
|
|
@@ -21,8 +25,6 @@ module Quickery
|
|
21
25
|
new_value = determine_quickery_value(depender_column_name)
|
22
26
|
update_columns(depender_column_name => new_value)
|
23
27
|
end
|
24
|
-
|
25
|
-
true
|
26
28
|
end
|
27
29
|
|
28
30
|
def determine_quickery_value(depender_column_name)
|
@@ -30,14 +32,10 @@ module Quickery
|
|
30
32
|
|
31
33
|
raise ArgumentError, "No defined quickery builder for #{depender_column_name}. Defined values are #{self.class.quickery_builders.keys}" unless quickery_builder
|
32
34
|
|
33
|
-
dependee_record = quickery_builder.
|
35
|
+
dependee_record = quickery_builder.association_chains.first.dependee_record(self)
|
34
36
|
dependee_record.send(quickery_builder.dependee_column_name)
|
35
37
|
end
|
36
38
|
end
|
37
39
|
end
|
38
40
|
end
|
39
41
|
end
|
40
|
-
|
41
|
-
ActiveSupport.on_load(:active_record) do
|
42
|
-
include Quickery::ActiveRecordExtensions::DSL
|
43
|
-
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Quickery
|
2
|
+
class AssociationChain
|
3
|
+
attr_accessor :quickery_builder
|
4
|
+
attr_reader :model
|
5
|
+
attr_reader :parent_association_chain
|
6
|
+
attr_reader :child_association_chain
|
7
|
+
attr_reader :name
|
8
|
+
attr_reader :dependee_column_name
|
9
|
+
attr_reader :belongs_to
|
10
|
+
|
11
|
+
def initialize(model:, parent_association_chain: nil, name: nil)
|
12
|
+
@model = model
|
13
|
+
@parent_association_chain = parent_association_chain
|
14
|
+
@name = name
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_children_association_chains(names_left:)
|
18
|
+
current_name = names_left.first
|
19
|
+
|
20
|
+
reflections = @model.reflections
|
21
|
+
column_names = @model.column_names
|
22
|
+
belongs_to_association_names = reflections.map{ |key, value| value.macro == :belongs_to ? key : nil }.compact
|
23
|
+
|
24
|
+
if belongs_to_association_names.include? current_name
|
25
|
+
@belongs_to = reflections[current_name]
|
26
|
+
@child_association_chain = AssociationChain.new(
|
27
|
+
model: belongs_to.class_name.constantize,
|
28
|
+
parent_association_chain: self,
|
29
|
+
name: current_name,
|
30
|
+
)
|
31
|
+
@child_association_chain.build_children_association_chains(names_left: names_left[1..-1])
|
32
|
+
return self
|
33
|
+
|
34
|
+
elsif column_names.include? current_name
|
35
|
+
@dependee_column_name = current_name
|
36
|
+
return self
|
37
|
+
|
38
|
+
else
|
39
|
+
raise Quickery::Errors::InvalidAssociationOrAttributeError.new(current_name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def child_association_chains(include_self: false, association_chains: [])
|
44
|
+
association_chains << self if include_self
|
45
|
+
|
46
|
+
if @child_association_chain.nil?
|
47
|
+
association_chains
|
48
|
+
else
|
49
|
+
association_chains << @child_association_chain
|
50
|
+
return @child_association_chain.child_association_chains(association_chains: association_chains)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def parent_association_chains(include_self: false, association_chains: [])
|
55
|
+
association_chains << self if include_self
|
56
|
+
|
57
|
+
if @parent_association_chain.nil?
|
58
|
+
association_chains
|
59
|
+
else
|
60
|
+
association_chains << @parent_association_chain
|
61
|
+
@parent_association_chain.parent_association_chains(association_chains: association_chains)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def joins_arg(current_joins_arg = nil)
|
66
|
+
if @parent_association_chain.nil?
|
67
|
+
current_joins_arg
|
68
|
+
else
|
69
|
+
if current_joins_arg.nil?
|
70
|
+
@parent_association_chain.joins_arg(@name.to_sym)
|
71
|
+
else
|
72
|
+
@parent_association_chain.joins_arg({ @name.to_sym => current_joins_arg })
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def dependee_record(from_record)
|
78
|
+
raise ArgumentError, 'argument should be an instance of @model' unless from_record.is_a? model
|
79
|
+
|
80
|
+
child_association_chains(include_self: true).inject(from_record) do |from_record, association_chain|
|
81
|
+
if association_chain.belongs_to
|
82
|
+
from_record.send(association_chain.belongs_to.name)
|
83
|
+
else
|
84
|
+
from_record
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def dependent_records(from_record)
|
90
|
+
primary_key_value = from_record.send(from_record.class.primary_key)
|
91
|
+
most_parent_model = parent_association_chains.last.model
|
92
|
+
|
93
|
+
records = most_parent_model.all
|
94
|
+
|
95
|
+
unless (joins_arg_tmp = joins_arg).nil?
|
96
|
+
records = records.joins(joins_arg_tmp)
|
97
|
+
end
|
98
|
+
|
99
|
+
records = records.where(
|
100
|
+
model.table_name => {
|
101
|
+
model.primary_key => primary_key_value
|
102
|
+
}
|
103
|
+
)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -2,105 +2,38 @@ module Quickery
|
|
2
2
|
class CallbacksBuilder
|
3
3
|
attr_reader :quickery_builder
|
4
4
|
|
5
|
-
def initialize(quickery_builder
|
5
|
+
def initialize(quickery_builder:)
|
6
6
|
@quickery_builder = quickery_builder
|
7
|
-
add_callbacks if should_add_callbacks
|
8
7
|
end
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
add_callback_to_dependee_model
|
15
|
-
add_callback_to_each_intermediate_model
|
9
|
+
def build_callbacks
|
10
|
+
build_callback_to_depender_model
|
11
|
+
build_callback_to_dependee_model
|
12
|
+
build_callback_to_each_intermediate_model
|
16
13
|
end
|
17
14
|
|
18
|
-
|
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
|
15
|
+
private
|
35
16
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
17
|
+
# add callback to immediately sync value after a record has been created / updated
|
18
|
+
def build_callback_to_depender_model
|
19
|
+
first_association_chain = @quickery_builder.association_chains.first
|
20
|
+
first_association_chain.model.quickery_association_chain_dependers ||= []
|
21
|
+
first_association_chain.model.quickery_association_chain_dependers << first_association_chain
|
40
22
|
end
|
41
23
|
|
42
24
|
# add callback to sync changes when dependee_column has been updated
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
25
|
+
def build_callback_to_dependee_model
|
26
|
+
last_association_chain = @quickery_builder.association_chains.last
|
27
|
+
last_association_chain.model.quickery_association_chain_dependees ||= []
|
28
|
+
last_association_chain.model.quickery_association_chain_dependees << last_association_chain
|
68
29
|
end
|
69
30
|
|
70
31
|
# also add callbacks to sync changes when intermediary associations have been changed (this does not include first and last builder)
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
32
|
+
def build_callback_to_each_intermediate_model
|
33
|
+
last_association_chain = @quickery_builder.association_chains.last
|
34
|
+
last_association_chain.parent_association_chains(include_self: true)[1..-2].each do |association_chain|
|
35
|
+
association_chain.model.quickery_association_chain_intermediaries ||= []
|
36
|
+
association_chain.model.quickery_association_chain_intermediaries << association_chain
|
104
37
|
end
|
105
38
|
end
|
106
39
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Quickery
|
2
|
+
class MappingsBuilder
|
3
|
+
attr_reader :model
|
4
|
+
attr_reader :mappings
|
5
|
+
|
6
|
+
def initialize(model:, mappings:)
|
7
|
+
@model = model
|
8
|
+
@mappings = mappings
|
9
|
+
end
|
10
|
+
|
11
|
+
# https://stackoverflow.com/questions/9647997/converting-a-nested-hash-into-a-flat-hash
|
12
|
+
def flat_hash(hash, k = [])
|
13
|
+
return {k => hash} unless hash.is_a?(Hash)
|
14
|
+
hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k + [v[0]]) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def map_attributes
|
18
|
+
flat_hash(@mappings).each do |names, depender_column_name|
|
19
|
+
first_association_chain = AssociationChain.new(model: model)
|
20
|
+
first_association_chain.build_children_association_chains(names_left: names)
|
21
|
+
|
22
|
+
all_association_chains = first_association_chain.child_association_chains(include_self: true)
|
23
|
+
dependee_column_name = all_association_chains.last.dependee_column_name
|
24
|
+
|
25
|
+
quickery_builder = QuickeryBuilder.new(
|
26
|
+
model: @model,
|
27
|
+
association_chains: all_association_chains,
|
28
|
+
dependee_column_name: dependee_column_name,
|
29
|
+
depender_column_name: depender_column_name,
|
30
|
+
)
|
31
|
+
|
32
|
+
quickery_builder.add_to_model
|
33
|
+
quickery_builder.add_to_association_chains
|
34
|
+
quickery_builder.create_model_callbacks
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -3,28 +3,32 @@ module Quickery
|
|
3
3
|
attr_reader :model
|
4
4
|
attr_reader :depender_column_name
|
5
5
|
attr_reader :dependee_column_name
|
6
|
-
attr_reader :
|
7
|
-
attr_reader :last_association_builder
|
8
|
-
attr_reader :callbacks_builder
|
6
|
+
attr_reader :association_chains
|
9
7
|
|
10
|
-
def initialize(dependee_column_name:,
|
8
|
+
def initialize(model:, association_chains:, dependee_column_name:, depender_column_name:)
|
9
|
+
@model = model
|
10
|
+
@association_chains = association_chains
|
11
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
12
|
@depender_column_name = depender_column_name
|
13
|
+
end
|
19
14
|
|
20
|
-
|
21
|
-
|
15
|
+
def add_to_model
|
22
16
|
define_quickery_builders_in_model_class unless @model.respond_to? :quickery_builders
|
23
|
-
|
24
17
|
# include this to the list of quickery builders defined for this model
|
25
18
|
@model.quickery_builders[depender_column_name] = self
|
26
19
|
end
|
27
20
|
|
21
|
+
def add_to_association_chains
|
22
|
+
association_chains.each do |association_chain|
|
23
|
+
association_chain.quickery_builder = self
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_model_callbacks
|
28
|
+
@callbacks_builder = CallbacksBuilder.new(quickery_builder: self)
|
29
|
+
@callbacks_builder.build_callbacks
|
30
|
+
end
|
31
|
+
|
28
32
|
private
|
29
33
|
|
30
34
|
def define_quickery_builders_in_model_class
|
data/lib/quickery/version.rb
CHANGED
@@ -7,6 +7,7 @@
|
|
7
7
|
* supports `has_many` and `belongs_to` while Quickery only supports `belongs_to` (currently)
|
8
8
|
* Cons against Quickery:
|
9
9
|
* It is not documented, but you'll need to do `Rails.application.eager_load!` first or something similar, to first load all the models as otherwise defined `persistize` across the models will be autoloaded, and therefore will not work properly. Quickery already does this eager loading of models out of the box. For more info, see [lib/quickery/railtie.rb](lib/quickery/railtie.rb)
|
10
|
+
* batch-update in one go for multiple quickery-defined attributes instead of updating each attribute/method
|
10
11
|
* loops through each children records and update each attribute which can get very slow with lots of children associated records, and therefore as many number of SQL UPDATE queries vs Quickery which uses `update_all` which is just one update query, and does not loop through children records:
|
11
12
|
|
12
13
|
```ruby
|
@@ -113,6 +114,7 @@
|
|
113
114
|
* allows "updates" as a background process
|
114
115
|
* Cons against Quickery:
|
115
116
|
* Rails 5 is not part of its supported list in their github page. And just to try it out on a Rails 5 app, `Flattery::ValueProvider` did not seem to work, because values are not pushed to the `:notes`'s `:category_name` values.
|
117
|
+
* batch-update in one go for multiple quickery-defined attributes instead of updating each
|
116
118
|
* Using Rails 4, does not support nested associated dependencies for `belongs_to`:
|
117
119
|
|
118
120
|
```ruby
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quickery
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jules Roman Polidario
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-10-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: railties
|
@@ -248,9 +248,13 @@ files:
|
|
248
248
|
- gemfiles/rails_4.gemfile
|
249
249
|
- gemfiles/rails_5.gemfile
|
250
250
|
- lib/quickery.rb
|
251
|
+
- lib/quickery/active_record_extensions.rb
|
252
|
+
- lib/quickery/active_record_extensions/callbacks.rb
|
251
253
|
- lib/quickery/active_record_extensions/dsl.rb
|
252
|
-
- lib/quickery/
|
254
|
+
- lib/quickery/association_chain.rb
|
253
255
|
- lib/quickery/callbacks_builder.rb
|
256
|
+
- lib/quickery/errors/invalid_association_or_attribute_error.rb
|
257
|
+
- lib/quickery/mappings_builder.rb
|
254
258
|
- lib/quickery/quickery_builder.rb
|
255
259
|
- lib/quickery/railtie.rb
|
256
260
|
- lib/quickery/version.rb
|
@@ -1,108 +0,0 @@
|
|
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
|