n1_loader 1.7.2 → 1.7.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md DELETED
@@ -1,276 +0,0 @@
1
- # N1Loader
2
-
3
- [![Gem Version][3]][4]
4
- [![][11]][12]
5
- [![][13]][14]
6
- [![][9]][10]
7
-
8
- N1Loader is designed to provide a simple way for avoiding [N+1 issues][7] of any kind.
9
- For example, it can help with resolving N+1 for:
10
- - database querying (most common case)
11
- - 3rd party service calls
12
- - complex calculations
13
- - and many more
14
-
15
- > If the project helps you or your organization, I would be very grateful if you [contribute][15] or [donate][10].
16
- > Your support is an incredible motivation and the biggest reward for my hard work.
17
-
18
- ___Support:___ ActiveRecord 5, 6, and 7.
19
-
20
- Follow me and stay tuned for the updates:
21
- - [LinkedIn](https://www.linkedin.com/in/evgeniydemin/)
22
- - [Medium](https://evgeniydemin.medium.com/)
23
- - [Twitter](https://twitter.com/EvgeniyDemin/)
24
- - [GitHub](https://github.com/djezzzl)
25
-
26
- ## Killer feature for GraphQL API
27
-
28
- N1Loader in combination with [ArLazyPreload][6] is a killer feature for your GraphQL API.
29
- 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!
30
-
31
- ```ruby
32
- gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
33
- ```
34
-
35
- ## Enhance [ActiveRecord][5]
36
-
37
- Are you working with well-known Rails application? Try it out and see how well N1Loader fulfills missing gaps when you can't define ActiveRecord associations!
38
- Check out the detailed [guide](guides/enhanced-activerecord.md) with examples or its [short version](examples/active_record_integration.rb).
39
-
40
- ```ruby
41
- gem 'n1_loader', require: 'n1_loader/active_record'
42
- ```
43
-
44
- Are you ready to forget about N+1 once and for all? Install [ArLazyPreload][6] and see dreams come true!
45
-
46
- ```ruby
47
- gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
48
- ```
49
-
50
- ## Standalone mode
51
-
52
- Are you not working with [ActiveRecord][5]? N1Loader is ready to be used as standalone solution! ([full snippet](examples/core.rb))
53
-
54
- ```ruby
55
- gem 'n1_loader'
56
- ```
57
-
58
- ## How to use it?
59
-
60
- N1Loader provides DSL that allows you to define N+1 ready loaders that can
61
- be injected into your objects in a way that you can avoid N+1 issues.
62
-
63
- > _Disclaimer_: examples below are working but designed to show N1Loader potentials only.
64
- In real live applications, N1Loader can be applied anywhere and in more [elegant way](examples/isolated_loader.rb).
65
-
66
- Let's look at simple example below ([full snippet](examples/active_record_integration.rb)):
67
- ```ruby
68
- class User < ActiveRecord::Base
69
- has_many :payments
70
-
71
- n1_optimized :payments_total do |users|
72
- total_per_user =
73
- Payment.group(:user_id)
74
- .where(user: users)
75
- .sum(:amount)
76
- .tap { |h| h.default = 0 }
77
-
78
- users.each do |user|
79
- total = total_per_user[user.id]
80
- fulfill(user, total)
81
- end
82
- end
83
- end
84
-
85
- class Payment < ActiveRecord::Base
86
- belongs_to :user
87
-
88
- validates :amount, presence: true
89
- end
90
-
91
- # A user has many payments.
92
- # Assuming, we want to know for group of users, what is a total of their payments, we can do the following:
93
-
94
- # Has N+1 issue
95
- p User.all.map { |user| user.payments.sum(&:amount) }
96
-
97
- # Has no N+1 but we load too many data that we don't actually need
98
- p User.all.includes(:payments).map { |user| user.payments.sum(&:amount) }
99
-
100
- # Has no N+1 and we load only what we need
101
- p User.all.includes(:payments_total).map { |user| user.payments_total }
102
- ```
103
-
104
- Let's assume now, that we want to calculate the total of payments for the given period for a group of users.
105
- N1Loader can do that as well! ([full snippet](examples/arguments_support.rb))
106
-
107
- ```ruby
108
- class User < ActiveRecord::Base
109
- has_many :payments
110
-
111
- n1_optimized :payments_total do
112
- argument :from
113
- argument :to
114
-
115
- def perform(users)
116
- total_per_user =
117
- Payment
118
- .group(:user_id)
119
- .where(created_at: from..to)
120
- .where(user: users)
121
- .sum(:amount)
122
- .tap { |h| h.default = 0 }
123
-
124
- users.each do |user|
125
- total = total_per_user[user.id]
126
- fulfill(user, total)
127
- end
128
- end
129
- end
130
- end
131
-
132
- class Payment < ActiveRecord::Base
133
- belongs_to :user
134
-
135
- validates :amount, presence: true
136
- end
137
-
138
- # Has N+1
139
- p User.all.map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }
140
-
141
- # Has no N+1 but we load too many data that we don't need
142
- p User.all.includes(:payments).map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }
143
-
144
- # Has no N+1 and calculation is the most efficient
145
- p User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) }
146
- ```
147
-
148
- ## Features and benefits
149
-
150
- - N1Loader doesn't use Promises which means it's easy to debug
151
- - Doesn't require injection to objects, can be used in [isolation](examples/isolated_loader.rb)
152
- - Loads data [lazily](examples/lazy_loading.rb)
153
- - Loaders can be [shared](examples/shared_loader.rb) between multiple classes
154
- - Loaded data can be [re-fetched](examples/reloading.rb)
155
- - Loader can be optimized for [single cases](examples/single_case.rb)
156
- - Loader support [arguments](examples/arguments_support.rb)
157
- - Has [integration](examples/active_record_integration.rb) with [ActiveRecord][5] which makes it brilliant
158
- - Has [integration](examples/ar_lazy_integration.rb) with [ArLazyPreload][6] which makes it excellent
159
-
160
- ### Feature killer for [ArLazyPreload][6] integration with isolated loaders
161
-
162
- In [version 1.6.0](CHANGELOG.md#160---20221019) isolated loaders were integrated with [ArLazyPreload][6] context.
163
- This means, it isn't required to inject `N1Loader` into your [ActiveRecord][5] models to avoid N+1 issues out of the box.
164
- It is especially great as many engineers are trying to avoid extra coupling between their models/services when it's possible.
165
- And this feature was designed exactly for this without losing an out of a box solution for N+1.
166
-
167
- Without further ado, please have a look at the [example](examples/ar_lazy_integration_with_isolated_loader.rb).
168
-
169
- _Spoiler:_ as soon as you have your loader defined, it will be as simple as `Loader.for(element)` to get your data efficiently and without N+1.
170
-
171
- ## Funding
172
-
173
- ### Open Collective Backers
174
-
175
- You're an individual who wants to support the project with a monthly donation. Your logo will be available on the Github page. [[Become a backer](https://opencollective.com/n1_loader#backer)]
176
-
177
- <a href="https://opencollective.com/n1_loader/backer/0/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/0/avatar.svg"></a>
178
- <a href="https://opencollective.com/n1_loader/backer/1/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/1/avatar.svg"></a>
179
- <a href="https://opencollective.com/n1_loader/backer/2/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/2/avatar.svg"></a>
180
- <a href="https://opencollective.com/n1_loader/backer/3/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/3/avatar.svg"></a>
181
- <a href="https://opencollective.com/n1_loader/backer/4/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/4/avatar.svg"></a>
182
- <a href="https://opencollective.com/n1_loader/backer/5/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/5/avatar.svg"></a>
183
- <a href="https://opencollective.com/n1_loader/backer/6/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/6/avatar.svg"></a>
184
- <a href="https://opencollective.com/n1_loader/backer/7/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/7/avatar.svg"></a>
185
- <a href="https://opencollective.com/n1_loader/backer/8/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/8/avatar.svg"></a>
186
- <a href="https://opencollective.com/n1_loader/backer/9/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/9/avatar.svg"></a>
187
- <a href="https://opencollective.com/n1_loader/backer/10/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/10/avatar.svg"></a>
188
- <a href="https://opencollective.com/n1_loader/backer/11/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/11/avatar.svg"></a>
189
- <a href="https://opencollective.com/n1_loader/backer/12/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/12/avatar.svg"></a>
190
- <a href="https://opencollective.com/n1_loader/backer/13/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/13/avatar.svg"></a>
191
- <a href="https://opencollective.com/n1_loader/backer/14/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/14/avatar.svg"></a>
192
- <a href="https://opencollective.com/n1_loader/backer/15/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/15/avatar.svg"></a>
193
- <a href="https://opencollective.com/n1_loader/backer/16/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/16/avatar.svg"></a>
194
- <a href="https://opencollective.com/n1_loader/backer/17/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/17/avatar.svg"></a>
195
- <a href="https://opencollective.com/n1_loader/backer/18/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/18/avatar.svg"></a>
196
- <a href="https://opencollective.com/n1_loader/backer/19/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/19/avatar.svg"></a>
197
- <a href="https://opencollective.com/n1_loader/backer/20/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/20/avatar.svg"></a>
198
- <a href="https://opencollective.com/n1_loader/backer/21/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/21/avatar.svg"></a>
199
- <a href="https://opencollective.com/n1_loader/backer/22/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/22/avatar.svg"></a>
200
- <a href="https://opencollective.com/n1_loader/backer/23/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/23/avatar.svg"></a>
201
- <a href="https://opencollective.com/n1_loader/backer/24/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/24/avatar.svg"></a>
202
- <a href="https://opencollective.com/n1_loader/backer/25/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/25/avatar.svg"></a>
203
- <a href="https://opencollective.com/n1_loader/backer/26/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/26/avatar.svg"></a>
204
- <a href="https://opencollective.com/n1_loader/backer/27/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/27/avatar.svg"></a>
205
- <a href="https://opencollective.com/n1_loader/backer/28/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/28/avatar.svg"></a>
206
- <a href="https://opencollective.com/n1_loader/backer/29/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/29/avatar.svg"></a>
207
-
208
- ### Open Collective Sponsors
209
-
210
- You're an organization that wants to support the project with a monthly donation. Your logo will be available on the Github page. [[Become a sponsor](https://opencollective.com/n1_loader#sponsor)]
211
-
212
- <a href="https://opencollective.com/n1_loader/sponsor/0/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/0/avatar.svg"></a>
213
- <a href="https://opencollective.com/n1_loader/sponsor/1/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/1/avatar.svg"></a>
214
- <a href="https://opencollective.com/n1_loader/sponsor/2/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/2/avatar.svg"></a>
215
- <a href="https://opencollective.com/n1_loader/sponsor/3/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/3/avatar.svg"></a>
216
- <a href="https://opencollective.com/n1_loader/sponsor/4/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/4/avatar.svg"></a>
217
- <a href="https://opencollective.com/n1_loader/sponsor/5/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/5/avatar.svg"></a>
218
- <a href="https://opencollective.com/n1_loader/sponsor/6/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/6/avatar.svg"></a>
219
- <a href="https://opencollective.com/n1_loader/sponsor/7/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/7/avatar.svg"></a>
220
- <a href="https://opencollective.com/n1_loader/sponsor/8/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/8/avatar.svg"></a>
221
- <a href="https://opencollective.com/n1_loader/sponsor/9/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/9/avatar.svg"></a>
222
- <a href="https://opencollective.com/n1_loader/sponsor/10/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/10/avatar.svg"></a>
223
- <a href="https://opencollective.com/n1_loader/sponsor/11/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/11/avatar.svg"></a>
224
- <a href="https://opencollective.com/n1_loader/sponsor/12/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/12/avatar.svg"></a>
225
- <a href="https://opencollective.com/n1_loader/sponsor/13/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/13/avatar.svg"></a>
226
- <a href="https://opencollective.com/n1_loader/sponsor/14/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/14/avatar.svg"></a>
227
- <a href="https://opencollective.com/n1_loader/sponsor/15/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/15/avatar.svg"></a>
228
- <a href="https://opencollective.com/n1_loader/sponsor/16/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/16/avatar.svg"></a>
229
- <a href="https://opencollective.com/n1_loader/sponsor/17/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/17/avatar.svg"></a>
230
- <a href="https://opencollective.com/n1_loader/sponsor/18/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/18/avatar.svg"></a>
231
- <a href="https://opencollective.com/n1_loader/sponsor/19/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/19/avatar.svg"></a>
232
- <a href="https://opencollective.com/n1_loader/sponsor/20/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/20/avatar.svg"></a>
233
- <a href="https://opencollective.com/n1_loader/sponsor/21/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/21/avatar.svg"></a>
234
- <a href="https://opencollective.com/n1_loader/sponsor/22/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/22/avatar.svg"></a>
235
- <a href="https://opencollective.com/n1_loader/sponsor/23/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/23/avatar.svg"></a>
236
- <a href="https://opencollective.com/n1_loader/sponsor/24/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/24/avatar.svg"></a>
237
- <a href="https://opencollective.com/n1_loader/sponsor/25/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/25/avatar.svg"></a>
238
- <a href="https://opencollective.com/n1_loader/sponsor/26/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/26/avatar.svg"></a>
239
- <a href="https://opencollective.com/n1_loader/sponsor/27/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/27/avatar.svg"></a>
240
- <a href="https://opencollective.com/n1_loader/sponsor/28/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/28/avatar.svg"></a>
241
- <a href="https://opencollective.com/n1_loader/sponsor/29/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/29/avatar.svg"></a>
242
-
243
- ## Contributing
244
-
245
- Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader.
246
- This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
247
-
248
- ## License
249
-
250
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
251
-
252
- ## Code of Conduct
253
-
254
- Everyone interacting in the N1Loader project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
255
-
256
- ## Changelog
257
-
258
- *N1Loader*'s changelog is available [here](CHANGELOG.md).
259
-
260
- ## Copyright
261
-
262
- Copyright (c) Evgeniy Demin. See [LICENSE.txt](LICENSE.txt) for further details.
263
-
264
- [3]: https://badge.fury.io/rb/n1_loader.svg
265
- [4]: https://badge.fury.io/rb/n1_loader
266
- [5]: https://github.com/rails/rails/tree/main/activerecord
267
- [6]: https://github.com/DmitryTsepelev/ar_lazy_preload
268
- [7]: https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping
269
- [8]: https://github.com/djezzzl/n1_loader
270
- [9]: https://opencollective.com/n1_loader/tiers/badge.svg
271
- [10]: https://opencollective.com/n1_loader#support
272
- [11]: https://github.com/djezzzl/n1_loader/actions/workflows/tests.yml/badge.svg?branch=master
273
- [12]: https://github.com/djezzzl/n1_loader/actions/workflows/tests.yml?query=event%3Aschedule
274
- [13]: https://github.com/djezzzl/n1_loader/actions/workflows/rubocop.yml/badge.svg?branch=master
275
- [14]: https://github.com/djezzzl/n1_loader/actions/workflows/rubocop.yml?query=event%3Aschedule
276
- [15]: https://github.com/djezzzl/n1_loader#contributing
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem "activerecord", "~> 5"
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem "activerecord", "~> 6"
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem "activerecord", "~> 7"
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem "ar_lazy_preload", "= 0.6.1"
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- gem "ar_lazy_preload", git: "https://github.com/DmitryTsepelev/ar_lazy_preload", branch: "master"
data/bin/console DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "bundler/setup"
5
- require "n1_loader"
6
-
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
-
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require "irb"
15
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
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
@@ -1,33 +0,0 @@
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)
@@ -1,36 +0,0 @@
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 and loads only required data
33
- p User.preload_associations_lazily.map(&:payments_total)
34
- # or
35
- ArLazyPreload.config.auto_preload = true
36
- User.all.map(&:payments_total)
@@ -1,39 +0,0 @@
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 Loader < N1Loader::Loader
9
- def perform(users)
10
- total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }
11
-
12
- users.each do |user|
13
- total = total_per_user[user.id]
14
- fulfill(user, total)
15
- end
16
- end
17
- end
18
-
19
- class User < ActiveRecord::Base
20
- has_many :payments
21
- end
22
-
23
- class Payment < ActiveRecord::Base
24
- belongs_to :user
25
-
26
- validates :amount, presence: true
27
- end
28
-
29
- fill_database
30
-
31
- # Has N+1 and loads redundant data
32
- p User.all.map { |user| user.payments.sum(&:amount) }
33
-
34
- # Has no N+1 and loads only required data
35
- p User.preload_associations_lazily.all.map { |user| Loader.for(user) }
36
-
37
- # or
38
- ArLazyPreload.config.auto_preload = true
39
- p User.all.map { |user| Loader.for(user) }
@@ -1,67 +0,0 @@
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) }
@@ -1,20 +0,0 @@
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
@@ -1,15 +0,0 @@
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
@@ -1,26 +0,0 @@
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 DELETED
@@ -1,39 +0,0 @@
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}"