n1_loader 1.7.2 → 1.7.3

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.
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}"