n1_loader 1.4.3 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe90c7df57ebf6237efd043aafd9a988415182d86ec877dedb02b1afb2b28295
4
- data.tar.gz: 0bd43d92e0de325a413f0f88680cc9cb9c79b2e165cbae1cfe7bd9b175c4ee2f
3
+ metadata.gz: 3e0025899b1fbd0329159dcd2e1ddf1d28318fe76354fd2e1378b803e49a9d96
4
+ data.tar.gz: 2f0f222cd9298bb56e2c3a9ae8c1f6559c7ec2bec3653c5a9a2c7c172b49b7fd
5
5
  SHA512:
6
- metadata.gz: d44ce33e6d822ee2121e5adf436c4841d92c89abc4fc98bace13371602987d21e813c530ddf1bf07b3fe5cbe42ba79bb71520a7161c37e95bd304dcca803d31d
7
- data.tar.gz: 1391dd9fa95f7e267e1e5c684a1dddbb025982f3fbf2c56207f1e868972b0ee4efafbca5919c781abf6a2bc8e85ce3d9251b8762d357d1bc82592b2d1ba540d4
6
+ metadata.gz: 48a55dd01386410a58a5fbf8ea9d556ac6fa2db33411a469b2c9f3fc99b9ccec2ad36eb482e0adba39a84af4ffd4adba6a51a8db23eba7869c03aca51add1d39
7
+ data.tar.gz: b4c275204251a8e6908e34af67beacd6bbc05a1fcd1168350ab538ecf48362c840c671d1223dfd3659b2fe87f1428ee0e012144c3c52fa11451a595a0b5a11d9
data/.gitignore CHANGED
@@ -8,4 +8,5 @@
8
8
  /tmp/
9
9
  /.idea
10
10
  .rspec_status
11
- Gemfile.lock
11
+ Gemfile.lock
12
+ .ruby-version
data/.rubocop.yml CHANGED
@@ -1,5 +1,7 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.5
3
+ Exclude:
4
+ - examples/**/*
3
5
 
4
6
  Style/StringLiterals:
5
7
  Enabled: true
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [1.4.4] - 2022/04/29
2
+
3
+ - Inject `N1Loader::Loadable` to `ActiveRecord::Base` automatically
4
+ - Make `reload` to call `n1_clear_cache`
5
+
1
6
  ## [1.4.3] - 2022-04-13
2
7
 
3
8
  - Add `default` support to arguments
data/README.md CHANGED
@@ -3,318 +3,147 @@
3
3
  [![CircleCI][1]][2]
4
4
  [![Gem Version][3]][4]
5
5
 
6
- Are you tired of fixing [N+1 issues][7]? Does it feel unnatural to you to fix it case by case in places where you need the data?
7
- We have a solution for you!
6
+ N1Loader is designed to provide a way for avoiding [N+1 issues][7] of any kind.
7
+ For example, it can help with resolving N+1 for:
8
+ - database querying (most common case)
9
+ - 3rd party service calls
10
+ - complex calculations
11
+ - and many more
8
12
 
9
- [N1Loader][8] is designed to solve the issue for good!
13
+ > [Toptal](https://www.toptal.com#snag-only-shrewd-web-development-experts) is hiring! [Join](https://www.toptal.com#snag-only-shrewd-web-development-experts) as Freelancer or [write me](mailto:lawliet.djez@gmail.com) if you want to join Core team.
10
14
 
11
- It has many benefits:
12
- - it can be [isolated](#isolated-loaders)
13
- - it loads data [lazily](#lazy-loading)
14
- - it supports [shareable loaders](#shareable-loaders) between multiple classes
15
- - it supports [reloading](#reloading)
16
- - it supports optimized [single object loading](#optimized-single-case)
17
- - it supports [arguments](#arguments)
18
- - it has an integration with [ActiveRecord][5] which makes it brilliant ([example](#activerecord))
19
- - it has an integration with [ArLazyPreload][6] which makes it excellent ([example](#arlazypreload))
15
+ ## Killer feature for GraphQL API
20
16
 
21
- ... and even more features to come! Stay tuned!
17
+ N1Loader in combination with [ArLazyPreload][6] is a killer feature for your GraphQL API.
18
+ Give it a try now and see incredible results instantly! Check out the [example](examples/graphql.rb) and start benefiting from it in your projects!
22
19
 
23
- ## Installation
24
-
25
- Add this line to your application's Gemfile:
26
-
27
- ```ruby
28
- gem 'n1_loader'
29
- ```
30
-
31
- You can add integration with [ActiveRecord][5] by:
32
- ```ruby
33
- gem 'n1_loader', require: 'n1_loader/active_record'
34
-
35
- # You also may be interested in injecting it to models
36
- class ActiveRecord::Base
37
- include N1Loader::Loadable
38
-
39
- def reload(*)
40
- n1_clear_cache
41
- super
42
- end
43
- end
44
- ```
45
-
46
- You can add the integration with [ActiveRecord][5] and [ArLazyPreload][6] by:
47
20
  ```ruby
48
21
  gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
49
-
50
- # You also may be interested in injecting it to models
51
- class ActiveRecord::Base
52
- include N1Loader::Loadable
53
-
54
- def reload(*)
55
- n1_clear_cache
56
- super
57
- end
58
- end
59
22
  ```
60
23
 
61
- ## Usage
24
+ ## Enhance [ActiveRecord][5]
62
25
 
63
- ```ruby
64
- class User
65
- include N1Loader::Loadable
66
-
67
- # with inline loader
68
- n1_optimized :orders_count do |users|
69
- orders_per_user = Order.where(user: users).group(:user_id).count
70
-
71
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
72
- end
73
- end
74
-
75
- # For single object
76
- user = User.new
77
- user.orders_count
26
+ Are you working with well-known Rails application? Try it out how well N1Loader fulfills missing gaps!
78
27
 
79
- # For multiple objects without N+1
80
- users = [User.new, User.new]
81
- N1Loader::Preloader.new(users).preload(:orders_count)
82
- users.map(&:orders_count)
28
+ ```ruby
29
+ gem 'n1_loader', require: 'n1_loader/active_record'
83
30
  ```
84
31
 
85
- ### Lazy loading
32
+ Are you ready to forget about N+1 once and for all? Install [ArLazyPreload][6] and see dreams come true!
86
33
 
87
34
  ```ruby
88
- class User
89
- include N1Loader::Loadable
90
-
91
- # with inline loader
92
- n1_optimized :orders_count do |users|
93
- orders_per_user = Order.where(user: users).group(:user_id).count
94
-
95
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
96
- end
97
- end
98
-
99
- user = User.new # => nothing was done for loading
100
- user.orders_count # => first time loading
101
-
102
- users = [User.new, User.new] # => nothing was done for loading
103
- N1Loader::Preloader.new([users]).preload(:orders_count) # => we only initialized loader but didn't perform it yet
104
- users.map(&:orders_count) # => loading has happen for the first time (without N+1)
35
+ gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
105
36
  ```
106
37
 
38
+ ## Standalone mode
107
39
 
108
- ### Shareable loaders
40
+ Are you not working with [ActiveRecord][5]? N1Loader is ready to be used as standalone solution! ([full snippet](examples/core.rb))
109
41
 
110
42
  ```ruby
111
- class OrdersCountLoader < N1Loader::Loader
112
- def perform(users)
113
- orders_per_user = Order.where(user: users).group(:user_id).count
114
-
115
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
116
- end
117
- end
118
-
119
- class User
120
- include N1Loader::Loadable
121
-
122
- n1_optimized :orders_count, OrdersCountLoader
123
- end
124
-
125
- class Customer
126
- include N1Loader::Loadable
127
-
128
- n1_optimized :orders_count, OrdersCountLoader
129
- end
130
-
131
- User.new.orders_count # => works
132
- Customer.new.orders_count # => works
43
+ gem 'n1_loader'
133
44
  ```
134
45
 
135
- ### Reloading
136
-
137
- ```ruby
138
- class User
139
- include N1Loader::Loadable
140
-
141
- # with inline loader
142
- n1_optimized :orders_count do |users|
143
- orders_per_user = Order.where(user: users).group(:user_id).count
144
-
145
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
146
- end
147
- end
148
-
149
- user = User.new
150
- user.orders_count # => loader is executed first time and value was cached
151
- user.orders_count(reload: true) # => loader is executed again and a new value was cached
152
- # or
153
- user.n1_clear_cache
154
- user.orders_count
155
-
156
- users = [User.new, User.new]
157
- N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized but not yet executed
158
- users.map(&:orders_count) # => loader was executed first time without N+1 issue and values were cached
46
+ ## How to use it?
159
47
 
160
- N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized again but not yet executed
161
- users.map(&:orders_count) # => new loader was executed first time without N+1 issue and new values were cached
162
- ```
48
+ N1Loader provides DSL that allows you to define N+1 ready loaders that can
49
+ be injected into your objects in a way that you can avoid N+1 issues.
163
50
 
164
- ### Isolated loaders
51
+ > _Disclaimer_: examples below are working but designed to show N1Loader potentials only.
52
+ In real live applications, N1Loader can be applied anywhere and in more [elegant way](examples/isolated_loader.rb).
165
53
 
54
+ Let's look at simple example below ([full snippet](examples/active_record_integration.rb)):
166
55
  ```ruby
167
- class IsolatedLoader < N1Loader::Loader
168
- def perform(elements)
169
- elements.each { |element| fulfill(element, [element]) }
56
+ class User < ActiveRecord::Base
57
+ has_many :payments
58
+
59
+ n1_optimized :payments_total do |users|
60
+ total_per_user =
61
+ Payment.group(:user_id)
62
+ .where(user: users)
63
+ .sum(:amount)
64
+ .tap { |h| h.default = 0 }
65
+
66
+ users.each do |user|
67
+ total = total_per_user[user.id]
68
+ fulfill(user, total)
69
+ end
170
70
  end
171
71
  end
172
72
 
173
- objects = [1, 2, 3, 4]
174
- loader = IsolatedLoader.new(objects)
175
- objects.each do |object|
176
- loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
73
+ class Payment < ActiveRecord::Base
74
+ belongs_to :user
75
+
76
+ validates :amount, presence: true
177
77
  end
178
- ```
179
78
 
180
- ### Optimized single case
79
+ # A user has many payments.
80
+ # Assuming, we want to know for group of users, what is a total of their payments, we can do the following:
181
81
 
182
- ```ruby
183
- class User
184
- include N1Loader::Loadable
82
+ # Has N+1 issue
83
+ p User.all.map { |user| user.payments.sum(&:amount) }
185
84
 
186
- n1_optimized :orders_count do # no arguments passed to the block, so we can override both perform and single.
187
- def perform(users)
188
- orders_per_user = Order.where(user: users).group(:user_id).count
85
+ # Has no N+1 but we load too many data that we don't actually need
86
+ p User.all.includes(:payments).map { |user| user.payments.sum(&:amount) }
189
87
 
190
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
191
- end
192
-
193
- # Optimized for single object loading
194
- def single(user)
195
- user.orders.count
196
- end
197
- end
198
- end
199
-
200
- user = User.new
201
- user.orders_count # single will be used here
202
-
203
- users = [User.new, User.new]
204
- N1Loader::Preloader.new(users).preload(:orders_count)
205
- users.map(&:orders_count) # perform will be used once without N+1
88
+ # Has no N+1 and we load only what we need
89
+ p User.all.includes(:payments_total).map { |user| user.payments_total }
206
90
  ```
207
91
 
208
- ### Arguments
92
+ Let's assume now, that we want to calculate the total of payments for the given period for a group of users.
93
+ N1Loader can do that as well! ([full snippet](examples/arguments_support.rb))
209
94
 
210
95
  ```ruby
211
- class User
212
- include N1Loader::Loadable
213
-
214
- n1_optimized :orders_count do
215
- argument :type
216
-
217
- def perform(users)
218
- orders_per_user = Order.where(type: type, user: users).group(:user_id).count
219
-
220
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
221
- end
222
- end
223
- end
224
-
225
- user = User.new
226
- user.orders_count(type: :gifts) # The loader will be performed first time for this argument
227
- user.orders_count(type: :sales) # The loader will be performed first time for this argument
228
- user.orders_count(type: :gifts) # The cached value will be used
229
-
230
- users = [User.new, User.new]
231
- N1Loader::Preloader.new(users).preload(:orders_count)
232
- users.map { |user| user.orders_count(type: :gifts) } # No N+1 here
233
- ```
96
+ class User < ActiveRecord::Base
97
+ has_many :payments
234
98
 
235
- _Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases,
236
- you may want to override it, for example:
99
+ n1_optimized :payments_total do
100
+ argument :from
101
+ argument :to
237
102
 
238
- ```ruby
239
- class User
240
- include N1Loader::Loadable
241
-
242
- n1_optimized :orders_count do
243
- argument :sale, optional: true, default: -> { Sale.last }
244
-
245
- cache_key { sale.id }
246
-
247
103
  def perform(users)
248
- orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
249
-
250
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
104
+ total_per_user =
105
+ Payment
106
+ .group(:user_id)
107
+ .where(created_at: from..to)
108
+ .where(user: users)
109
+ .sum(:amount)
110
+ .tap { |h| h.default = 0 }
111
+
112
+ users.each do |user|
113
+ total = total_per_user[user.id]
114
+ fulfill(user, total)
115
+ end
251
116
  end
252
117
  end
253
118
  end
254
119
 
255
- user = User.new
256
- user.orders_count(sale: Sale.first) # perform will be executed and value will be cached
257
- user.orders_count(sale: Sale.first) # the cached value will be returned
258
- ```
259
-
260
-
261
- ## Integrations
262
-
263
- ### [ActiveRecord][5]
264
-
265
- _Note_: Rails 7 support is coming soon! Stay tuned!
266
-
267
- ```ruby
268
- class User < ActiveRecord::Base
269
- include N1Loader::Loadable
270
-
271
- n1_optimized :orders_count do |users|
272
- orders_per_user = Order.where(user: users).group(:user_id).count
120
+ class Payment < ActiveRecord::Base
121
+ belongs_to :user
273
122
 
274
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
275
- end
123
+ validates :amount, presence: true
276
124
  end
277
125
 
278
- # For single user
279
- user = User.first
280
- user.orders_count
281
-
282
- # For many users without N+1
283
- User.limit(5).includes(:orders_count).map(&:orders_count)
126
+ # Has N+1
127
+ p User.all.map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }
284
128
 
285
- # or with explicit preloader
286
- users = User.limit(5).to_a
287
- N1Loader::Preloader.new(users).preload(:orders_count)
129
+ # Has no N+1 but we load too many data that we don't need
130
+ p User.all.includes(:payments).map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }
288
131
 
289
- # No N+1 here
290
- users.map(&:orders_count)
132
+ # Has no N+1 and calculation is the most efficient
133
+ p User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) }
291
134
  ```
292
135
 
293
- ### [ArLazyPreload][6]
294
-
295
- ```ruby
296
- class User < ActiveRecord::Base
297
- include N1Loader::Loadable
298
-
299
- n1_optimized :orders_count do |users|
300
- orders_per_user = Order.where(user: users).group(:user_id).count
301
-
302
- users.each { |user| fulfill(user, orders_per_user[user.id]) }
303
- end
304
- end
305
-
306
- # For single user
307
- user = User.first
308
- user.orders_count
309
-
310
- # For many users without N+1
311
- User.lazy_preload(:orders_count).all.map(&:orders_count)
312
- # or
313
- User.preload_associations_lazily.all.map(&:orders_count)
314
- # or
315
- ArLazyPreload.config.auto_preload = true
316
- User.all.map(:orders_count)
317
- ```
136
+ ## Features and benefits
137
+
138
+ - N1Loader doesn't use Promises which means it's easy to debug
139
+ - Doesn't require injection to objects, can be used in [isolation](examples/isolated_loader.rb)
140
+ - Loads data [lazily](examples/lazy_loading.rb)
141
+ - Loaders can be [shared](examples/shared_loader.rb) between multiple classes
142
+ - Loaded data can be [re-fetched](examples/reloading.rb)
143
+ - Loader can be optimized for [single cases](examples/single_case.rb)
144
+ - Loader support [arguments](examples/arguments_support.rb)
145
+ - Has [integration](examples/active_record_integration.rb) with [ActiveRecord][5] which makes it brilliant
146
+ - Has [integration](examples/ar_lazy_integration.rb) with [ArLazyPreload][6] which makes it excellent
318
147
 
319
148
  ## Contributing
320
149
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n1_loader/active_record"
4
+
5
+ require_relative 'context/setup_database'
6
+
7
+ class User < ActiveRecord::Base
8
+ has_many :payments
9
+
10
+ n1_optimized :payments_total do |users|
11
+ total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }
12
+
13
+ users.each do |user|
14
+ total = total_per_user[user.id]
15
+ fulfill(user, total)
16
+ end
17
+ end
18
+ end
19
+
20
+ class Payment < ActiveRecord::Base
21
+ belongs_to :user
22
+
23
+ validates :amount, presence: true
24
+ end
25
+
26
+ fill_database
27
+
28
+ # Has N+1
29
+ p User.all.map { |user| user.payments.sum(&:amount) }
30
+ # Has no N+1 but we load too many data that we don't need
31
+ p User.all.includes(:payments).map { |user| user.payments.sum(&:amount) }
32
+ # Has no N+1 and calculation is the most efficient
33
+ p User.all.includes(:payments_total).map(&:payments_total)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n1_loader/ar_lazy_preload"
4
+
5
+ require_relative 'context/setup_ar_lazy'
6
+ require_relative 'context/setup_database'
7
+
8
+ class User < ActiveRecord::Base
9
+ has_many :payments
10
+
11
+ n1_optimized :payments_total do |users|
12
+ total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }
13
+
14
+ users.each do |user|
15
+ total = total_per_user[user.id]
16
+ fulfill(user, total)
17
+ end
18
+ end
19
+ end
20
+
21
+ class Payment < ActiveRecord::Base
22
+ belongs_to :user
23
+
24
+ validates :amount, presence: true
25
+ end
26
+
27
+ fill_database
28
+
29
+ # Has N+1
30
+ p User.all.map { |user| user.payments.sum(&:amount) }
31
+
32
+ # Has no N+1 but we load too many data that we don't need
33
+ p User.preload_associations_lazily.map(&:payments_total)
34
+
35
+ # Has no N+1 and calculation is the most efficient
36
+ ArLazyPreload.config.auto_preload = true
37
+ User.all.map(&:payments_total)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n1_loader/active_record"
4
+
5
+ require_relative 'context/setup_database'
6
+
7
+ class User < ActiveRecord::Base
8
+ has_many :payments
9
+
10
+ n1_optimized :payments_total do
11
+ # Arguments can be:
12
+ # argument :something, optional: true
13
+ # argument :something, default: -> { 100 }
14
+ #
15
+ # Note: do not use mutable (mostly timing related) defaults like:
16
+ # argument :from, default -> { 2.minutes.from_now }
17
+ # because such values will be unique for every loader call which will make N+1 issue stay
18
+ argument :from
19
+ argument :to
20
+
21
+ # This is used to define logic how loaders are compared to each other
22
+ # default is:
23
+ # cache_key { *arguments.map(&:object_id) }
24
+ cache_key { [from, to] }
25
+
26
+ def perform(users)
27
+ total_per_user =
28
+ Payment
29
+ .group(:user_id)
30
+ .where(created_at: from..to)
31
+ .where(user: users)
32
+ .sum(:amount)
33
+ .tap { |h| h.default = 0 }
34
+
35
+ users.each do |user|
36
+ total = total_per_user[user.id]
37
+ fulfill(user, total)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class Payment < ActiveRecord::Base
44
+ belongs_to :user
45
+
46
+ validates :amount, presence: true
47
+ end
48
+
49
+ fill_database
50
+
51
+ from = 2.days.ago
52
+ to = 1.day.ago
53
+
54
+ # Has N+1
55
+ p User.all.map { |user|
56
+ user.payments.select do |payment|
57
+ payment.created_at >= from && payment.created_at <= to
58
+ end.sum(&:amount)
59
+ }
60
+ # Has no N+1 but we load too many data that we don't need
61
+ p User.all.includes(:payments).map { |user|
62
+ user.payments.select do |payment|
63
+ payment.created_at >= from && payment.created_at <= to
64
+ end.sum(&:amount)
65
+ }
66
+ # Has no N+1 and calculation is the most efficient
67
+ p User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) }
@@ -0,0 +1,20 @@
1
+ # 3rd party service, or database, or anything else that can perform in batches
2
+ class Service
3
+ def self.count
4
+ @count ||= 0
5
+ end
6
+
7
+ def self.increase!
8
+ @count = (@count || 0) + 1
9
+ end
10
+
11
+ def self.receive(*users)
12
+ increase!
13
+
14
+ users.flatten.map(&:object_id)
15
+ end
16
+
17
+ def self.single(user)
18
+ user.object_id
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ ActiveSupport.on_load(:active_record) do
2
+ ActiveRecord::Base.include(ArLazyPreload::Base)
3
+
4
+ ActiveRecord::Relation.prepend(ArLazyPreload::Relation)
5
+ ActiveRecord::AssociationRelation.prepend(ArLazyPreload::AssociationRelation)
6
+ ActiveRecord::Relation::Merger.prepend(ArLazyPreload::Merger)
7
+
8
+ [
9
+ ActiveRecord::Associations::CollectionAssociation,
10
+ ActiveRecord::Associations::Association
11
+ ].each { |klass| klass.prepend(ArLazyPreload::Association) }
12
+
13
+ ActiveRecord::Associations::CollectionAssociation.prepend(ArLazyPreload::CollectionAssociation)
14
+ ActiveRecord::Associations::CollectionProxy.prepend(ArLazyPreload::CollectionProxy)
15
+ end
@@ -0,0 +1,26 @@
1
+ require "sqlite3"
2
+
3
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
4
+ ActiveRecord::Base.connection.tables.each do |table|
5
+ ActiveRecord::Base.connection.drop_table(table, force: :cascade)
6
+ end
7
+ ActiveRecord::Schema.verbose = false
8
+ ActiveRecord::Base.logger = Logger.new($stdout)
9
+
10
+ ActiveRecord::Schema.define(version: 1) do
11
+ create_table(:payments) do |t|
12
+ t.belongs_to :user
13
+ t.integer :amount
14
+ t.timestamps
15
+ end
16
+ create_table(:users)
17
+ end
18
+
19
+ def fill_database
20
+ 10.times do
21
+ user = User.create!
22
+ 10.times do
23
+ Payment.create!(user: user, amount: rand(1000))
24
+ end
25
+ end
26
+ end
data/examples/core.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n1_loader"
4
+
5
+ require_relative 'context/service'
6
+
7
+ # Class that wants to request 3rd party service without N+1
8
+ class User
9
+ include N1Loader::Loadable
10
+
11
+ def unoptimized_call
12
+ Service.receive(self)[0]
13
+ end
14
+
15
+ n1_optimized :optimized_call do |users|
16
+ data = Service.receive(users)
17
+
18
+ users.each_with_index do |user, index|
19
+ fulfill(user, data[index])
20
+ end
21
+ end
22
+ end
23
+
24
+ # works fine for single case
25
+ user = User.new
26
+ p "Works correctly: #{user.unoptimized_call == user.optimized_call}"
27
+
28
+ users = [User.new, User.new]
29
+
30
+ # Has N+1
31
+ count_before = Service.count
32
+ p users.map(&:unoptimized_call)
33
+ p "Has N+1 #{Service.count == count_before + users.count}"
34
+
35
+ # Has no N+1
36
+ count_before = Service.count
37
+ N1Loader::Preloader.new(users).preload(:optimized_call)
38
+ p users.map(&:optimized_call)
39
+ p "Has no N+1: #{Service.count == count_before + 1}"
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "n1_loader/ar_lazy_preload"
4
+ require 'graphql'
5
+
6
+ require_relative 'context/setup_database'
7
+ require_relative 'context/setup_ar_lazy'
8
+
9
+ class User < ActiveRecord::Base
10
+ has_many :payments
11
+
12
+ n1_optimized :payments_total do |users|
13
+ total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }
14
+
15
+ users.each do |user|
16
+ total = total_per_user[user.id]
17
+ fulfill(user, total)
18
+ end
19
+ end
20
+ end
21
+
22
+ class Payment < ActiveRecord::Base
23
+ belongs_to :user
24
+
25
+ validates :amount, presence: true
26
+ end
27
+
28
+ 10.times do
29
+ user = User.create!
30
+ 10.times do
31
+ Payment.create!(user: user, amount: rand(1000))
32
+ end
33
+ end
34
+
35
+ ArLazyPreload.config.auto_preload = true
36
+ # Or use +preload_associations_lazily+ when loading objects from database
37
+
38
+ class UserType < GraphQL::Schema::Object
39
+ field :payments_total, Integer
40
+ end
41
+
42
+ class QueryType < GraphQL::Schema::Object
43
+ field :users, [UserType]
44
+
45
+ def users
46
+ User.all
47
+ end
48
+ end
49
+
50
+ class Schema < GraphQL::Schema
51
+ query QueryType
52
+ end
53
+
54
+ query_string = <<~GQL
55
+ {
56
+ users {
57
+ paymentsTotal
58
+ }
59
+ }
60
+ GQL
61
+
62
+ # No N+1. And never will be!
63
+ p Schema.execute(query_string)['data']
@@ -0,0 +1,13 @@
1
+ require 'n1_loader'
2
+
3
+ class IsolatedLoader < N1Loader::Loader
4
+ def perform(elements)
5
+ elements.each { |element| fulfill(element, [element]) }
6
+ end
7
+ end
8
+
9
+ objects = [1, 2, 3, 4]
10
+ loader = IsolatedLoader.new(objects)
11
+ objects.each do |object|
12
+ loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
13
+ end
@@ -0,0 +1,26 @@
1
+ require 'n1_loader'
2
+
3
+ require_relative 'context/service'
4
+
5
+ # Class that wants to request 3rd party service without N+1
6
+ class User
7
+ include N1Loader::Loadable
8
+
9
+ n1_optimized :optimized_call do |users|
10
+ data = Service.receive(users)
11
+
12
+ users.each_with_index do |user, index|
13
+ fulfill(user, data[index])
14
+ end
15
+ end
16
+ end
17
+
18
+ users = [User.new, User.new, User.new]
19
+
20
+ # Initialized loader but didn't perform it yet
21
+ N1Loader::Preloader.new(users).preload(:optimized_call)
22
+ p "No calls yet: #{Service.count == 0}"
23
+
24
+ # First time loading
25
+ users.map(&:optimized_call)
26
+ p "First time loaded: #{Service.count == 1}"
@@ -0,0 +1,32 @@
1
+ require 'n1_loader'
2
+
3
+ require_relative 'context/service'
4
+
5
+ class User
6
+ include N1Loader::Loadable
7
+
8
+ n1_optimized :optimized_call do |users|
9
+ data = Service.receive(users)
10
+
11
+ users.each_with_index do |user, index|
12
+ fulfill(user, data[index])
13
+ end
14
+ end
15
+ end
16
+
17
+ users = [User.new, User.new, User.new]
18
+
19
+ # Initialized loader but didn't perform it yet
20
+ N1Loader::Preloader.new(users).preload(:optimized_call)
21
+ p "No calls yet: #{Service.count == 0}"
22
+
23
+ # First time loading
24
+ users.map(&:optimized_call)
25
+ p "First time loaded: #{Service.count == 1}"
26
+
27
+ users.first.optimized_call(reload: true)
28
+ p "Reloaded for this object only: #{Service.count == 2}"
29
+
30
+ users.first.n1_clear_cache
31
+ users.first.optimized_call
32
+ p "Reloaded for this object only: #{Service.count == 3}"
@@ -0,0 +1,34 @@
1
+ require 'n1_loader'
2
+
3
+ require_relative 'context/service'
4
+
5
+ # Loader that will be shared between multiple classes
6
+ class SharedLoader < N1Loader::Loader
7
+ def perform(objects)
8
+ data = Service.receive(objects)
9
+
10
+ objects.each_with_index do |user, index|
11
+ fulfill(user, data[index])
12
+ end
13
+ end
14
+ end
15
+
16
+ class User
17
+ include N1Loader::Loadable
18
+
19
+ n1_optimized :optimized_call, SharedLoader
20
+ end
21
+
22
+ class Payment
23
+ include N1Loader::Loadable
24
+
25
+ n1_optimized :optimized_call, SharedLoader
26
+ end
27
+
28
+ objects = [User.new, Payment.new, User.new, Payment.new]
29
+
30
+ N1Loader::Preloader.new(objects).preload(:optimized_call)
31
+
32
+ # First time loading for all objects
33
+ objects.map(&:optimized_call)
34
+ p "Loaded for all once: #{Service.count == 1}"
@@ -0,0 +1,34 @@
1
+ require 'n1_loader'
2
+
3
+ require_relative 'context/service'
4
+
5
+ # Loader that will be shared between multiple classes
6
+ class OptimizedLoader < N1Loader::Loader
7
+ def perform(objects)
8
+ data = Service.receive(objects)
9
+
10
+ objects.each_with_index do |user, index|
11
+ fulfill(user, data[index])
12
+ end
13
+ end
14
+
15
+ def single(object)
16
+ Service.single(object)
17
+ end
18
+ end
19
+
20
+ class User
21
+ include N1Loader::Loadable
22
+
23
+ n1_optimized :optimized_call, OptimizedLoader
24
+ end
25
+
26
+ objects = [User.new, User.new]
27
+
28
+ N1Loader::Preloader.new(objects).preload(:optimized_call)
29
+
30
+ objects.map(&:optimized_call)
31
+ p "Used multi-case perform: #{Service.count == 1}"
32
+
33
+ User.new.optimized_call
34
+ p "Used single-case perform: #{Service.count == 1}"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module N1Loader
4
+ module ActiveRecord
5
+ # Extension module for ActiveRecord::Base
6
+ module Base
7
+ extend ActiveSupport::Concern
8
+
9
+ include N1Loader::Loadable
10
+
11
+ # Clear N1Loader cache on reloading the object
12
+ def reload(*)
13
+ n1_clear_cache
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
@@ -16,6 +16,7 @@ end
16
16
  ActiveSupport.on_load(:active_record) do
17
17
  require_relative "active_record/loader"
18
18
  require_relative "active_record/loader_collection"
19
+ require_relative "active_record/base"
19
20
 
20
21
  case ActiveRecord::VERSION::MAJOR
21
22
  when 6
@@ -25,4 +26,5 @@ ActiveSupport.on_load(:active_record) do
25
26
  end
26
27
 
27
28
  ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
29
+ ActiveRecord::Base.include(N1Loader::ActiveRecord::Base)
28
30
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module N1Loader
4
- VERSION = "1.4.3"
4
+ VERSION = "1.4.4"
5
5
  end
data/n1_loader.gemspec CHANGED
@@ -22,9 +22,10 @@ Gem::Specification.new do |spec|
22
22
  end
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_development_dependency "activerecord", ">= 5"
25
+ spec.add_development_dependency "activerecord", ">= 5", "< 7"
26
26
  spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
27
27
  spec.add_development_dependency "db-query-matchers", "~> 0.10"
28
+ spec.add_development_dependency "graphql", "~> 2.0"
28
29
  spec.add_development_dependency "rails", ">= 5"
29
30
  spec.add_development_dependency "rspec", "~> 3.0"
30
31
  spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n1_loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 1.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-13 00:00:00.000000000 Z
11
+ date: 2022-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -17,6 +17,9 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '5'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7'
20
23
  type: :development
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,6 +27,9 @@ dependencies:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: '5'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: ar_lazy_preload
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +58,20 @@ dependencies:
52
58
  - - "~>"
53
59
  - !ruby/object:Gem::Version
54
60
  version: '0.10'
61
+ - !ruby/object:Gem::Dependency
62
+ name: graphql
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
55
75
  - !ruby/object:Gem::Dependency
56
76
  name: rails
57
77
  requirement: !ruby/object:Gem::Requirement
@@ -145,10 +165,24 @@ files:
145
165
  - ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile
146
166
  - bin/console
147
167
  - bin/setup
168
+ - examples/active_record_integration.rb
169
+ - examples/ar_lazy_integration.rb
170
+ - examples/arguments_support.rb
171
+ - examples/context/service.rb
172
+ - examples/context/setup_ar_lazy.rb
173
+ - examples/context/setup_database.rb
174
+ - examples/core.rb
175
+ - examples/graphql.rb
176
+ - examples/isolated_loader.rb
177
+ - examples/lazy_loading.rb
178
+ - examples/reloading.rb
179
+ - examples/shared_loader.rb
180
+ - examples/single_case.rb
148
181
  - lib/n1_loader.rb
149
182
  - lib/n1_loader/active_record.rb
150
183
  - lib/n1_loader/active_record/associations_preloader_v5.rb
151
184
  - lib/n1_loader/active_record/associations_preloader_v6.rb
185
+ - lib/n1_loader/active_record/base.rb
152
186
  - lib/n1_loader/active_record/loader.rb
153
187
  - lib/n1_loader/active_record/loader_collection.rb
154
188
  - lib/n1_loader/ar_lazy_preload.rb