n1_loader 1.4.4 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +17 -11
- data/CHANGELOG.md +8 -0
- data/README.md +81 -3
- data/activerecord-gemfiles/ar_7_latest.gemfile +3 -0
- data/guides/enhanced-activerecord.md +266 -0
- data/lib/n1_loader/active_record/associations_preloader_v7.rb +40 -0
- data/lib/n1_loader/active_record/loader_collection.rb +8 -0
- data/lib/n1_loader/active_record.rb +6 -2
- data/lib/n1_loader/core/loader.rb +3 -1
- data/lib/n1_loader/version.rb +1 -1
- data/n1_loader.gemspec +4 -4
- metadata +10 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eef4b4cb71bedafe0820631695d26d1782f11d940899c8cdd2f745179197e489
|
4
|
+
data.tar.gz: 909035eceab471c23057fc075a719f22ab8d2138c114c92500153cc19178f8c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a47920e7df24aefbd413b8c2f2fdd6fd4e5918481109b8b79af01070d9c0f262784df0410ad6e758753405acaa7ffcc6a9dd2fc22eeab51cd5377885473b842
|
7
|
+
data.tar.gz: 8139a30fc4b15740c403aeee3791e56a4268d1890d0cb4aedd4d9d29e9b54c26a1e98541b2865932a005abf7d854b7461c0de4c3322beb7e08124f97fb75a11c
|
data/.circleci/config.yml
CHANGED
@@ -100,36 +100,42 @@ workflows:
|
|
100
100
|
matrix:
|
101
101
|
parameters:
|
102
102
|
ruby-version: [
|
103
|
-
"2.5",
|
104
103
|
"2.7",
|
105
104
|
"latest"
|
106
105
|
]
|
107
106
|
activerecord-gemfile: [
|
108
107
|
"ar_5_latest",
|
109
|
-
"ar_6_latest"
|
108
|
+
"ar_6_latest",
|
109
|
+
"ar_7_latest"
|
110
110
|
]
|
111
111
|
ar_lazy_preload-gemfile: [
|
112
112
|
"ar_lazy_preload_0.6.1",
|
113
113
|
"ar_lazy_preload_master"
|
114
114
|
]
|
115
115
|
exclude:
|
116
|
-
# Ruby 2.5 and AR Lazy Preload 1+
|
117
|
-
- ruby-version: "2.5"
|
118
|
-
activerecord-gemfile: "ar_5_latest"
|
119
|
-
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
120
|
-
|
121
|
-
- ruby-version: "2.5"
|
122
|
-
activerecord-gemfile: "ar_6_latest"
|
123
|
-
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
124
|
-
|
125
116
|
# AR 5 and ruby 3+
|
126
117
|
- ruby-version: "latest"
|
127
118
|
activerecord-gemfile: "ar_5_latest"
|
128
119
|
ar_lazy_preload-gemfile: "ar_lazy_preload_0.6.1"
|
120
|
+
|
129
121
|
- ruby-version: "latest"
|
130
122
|
activerecord-gemfile: "ar_5_latest"
|
131
123
|
ar_lazy_preload-gemfile: "ar_lazy_preload_master"
|
132
124
|
|
125
|
+
# AR 7 and ar_lazy_preload < 1
|
126
|
+
- ruby-version: "2.5"
|
127
|
+
activerecord-gemfile: "ar_7_latest"
|
128
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_0.6.1"
|
129
|
+
|
130
|
+
- ruby-version: "2.7"
|
131
|
+
activerecord-gemfile: "ar_7_latest"
|
132
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_0.6.1"
|
133
|
+
|
134
|
+
- ruby-version: "latest"
|
135
|
+
activerecord-gemfile: "ar_7_latest"
|
136
|
+
ar_lazy_preload-gemfile: "ar_lazy_preload_0.6.1"
|
137
|
+
|
138
|
+
|
133
139
|
name: ruby-<< matrix.ruby-version >>-<< matrix.activerecord-gemfile >>-<< matrix.ar_lazy_preload-gemfile >>
|
134
140
|
- rubocop:
|
135
141
|
requires:
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## [1.5.1] - 2022/09/20
|
2
|
+
|
3
|
+
- Fix support of falsey value of arguments. Thanks [Aitor Lopez Beltran](https://github.com/aitorlb) for the [contribution](https://github.com/djezzzl/n1_loader/pull/23)!
|
4
|
+
|
5
|
+
## [1.5.0] - 2022/05/01
|
6
|
+
|
7
|
+
- Add support of Rails 7
|
8
|
+
|
1
9
|
## [1.4.4] - 2022/04/29
|
2
10
|
|
3
11
|
- Inject `N1Loader::Loadable` to `ActiveRecord::Base` automatically
|
data/README.md
CHANGED
@@ -2,8 +2,9 @@
|
|
2
2
|
|
3
3
|
[![CircleCI][1]][2]
|
4
4
|
[![Gem Version][3]][4]
|
5
|
+
[![][9]][10]
|
5
6
|
|
6
|
-
N1Loader is designed to provide a way for avoiding [N+1 issues][7] of any kind.
|
7
|
+
N1Loader is designed to provide a simple way for avoiding [N+1 issues][7] of any kind.
|
7
8
|
For example, it can help with resolving N+1 for:
|
8
9
|
- database querying (most common case)
|
9
10
|
- 3rd party service calls
|
@@ -12,6 +13,8 @@ For example, it can help with resolving N+1 for:
|
|
12
13
|
|
13
14
|
> [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.
|
14
15
|
|
16
|
+
___Support:___ ActiveRecord 5, 6, and 7.
|
17
|
+
|
15
18
|
## Killer feature for GraphQL API
|
16
19
|
|
17
20
|
N1Loader in combination with [ArLazyPreload][6] is a killer feature for your GraphQL API.
|
@@ -23,7 +26,8 @@ gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
|
|
23
26
|
|
24
27
|
## Enhance [ActiveRecord][5]
|
25
28
|
|
26
|
-
Are you working with well-known Rails application? Try it out how well N1Loader fulfills missing gaps!
|
29
|
+
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!
|
30
|
+
Check out the detailed [guide](guides/enhanced-activerecord.md) with examples or its [short version](examples/active_record_integration.rb).
|
27
31
|
|
28
32
|
```ruby
|
29
33
|
gem 'n1_loader', require: 'n1_loader/active_record'
|
@@ -145,6 +149,78 @@ p User.all.includes(:payments_total).map { |user| user.payments_total(from: from
|
|
145
149
|
- Has [integration](examples/active_record_integration.rb) with [ActiveRecord][5] which makes it brilliant
|
146
150
|
- Has [integration](examples/ar_lazy_integration.rb) with [ArLazyPreload][6] which makes it excellent
|
147
151
|
|
152
|
+
## Funding
|
153
|
+
|
154
|
+
### Open Collective Backers
|
155
|
+
|
156
|
+
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)]
|
157
|
+
|
158
|
+
<a href="https://opencollective.com/n1_loader/backer/0/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/0/avatar.svg"></a>
|
159
|
+
<a href="https://opencollective.com/n1_loader/backer/1/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/1/avatar.svg"></a>
|
160
|
+
<a href="https://opencollective.com/n1_loader/backer/2/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/2/avatar.svg"></a>
|
161
|
+
<a href="https://opencollective.com/n1_loader/backer/3/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/3/avatar.svg"></a>
|
162
|
+
<a href="https://opencollective.com/n1_loader/backer/4/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/4/avatar.svg"></a>
|
163
|
+
<a href="https://opencollective.com/n1_loader/backer/5/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/5/avatar.svg"></a>
|
164
|
+
<a href="https://opencollective.com/n1_loader/backer/6/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/6/avatar.svg"></a>
|
165
|
+
<a href="https://opencollective.com/n1_loader/backer/7/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/7/avatar.svg"></a>
|
166
|
+
<a href="https://opencollective.com/n1_loader/backer/8/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/8/avatar.svg"></a>
|
167
|
+
<a href="https://opencollective.com/n1_loader/backer/9/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/9/avatar.svg"></a>
|
168
|
+
<a href="https://opencollective.com/n1_loader/backer/10/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/10/avatar.svg"></a>
|
169
|
+
<a href="https://opencollective.com/n1_loader/backer/11/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/11/avatar.svg"></a>
|
170
|
+
<a href="https://opencollective.com/n1_loader/backer/12/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/12/avatar.svg"></a>
|
171
|
+
<a href="https://opencollective.com/n1_loader/backer/13/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/13/avatar.svg"></a>
|
172
|
+
<a href="https://opencollective.com/n1_loader/backer/14/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/14/avatar.svg"></a>
|
173
|
+
<a href="https://opencollective.com/n1_loader/backer/15/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/15/avatar.svg"></a>
|
174
|
+
<a href="https://opencollective.com/n1_loader/backer/16/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/16/avatar.svg"></a>
|
175
|
+
<a href="https://opencollective.com/n1_loader/backer/17/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/17/avatar.svg"></a>
|
176
|
+
<a href="https://opencollective.com/n1_loader/backer/18/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/18/avatar.svg"></a>
|
177
|
+
<a href="https://opencollective.com/n1_loader/backer/19/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/19/avatar.svg"></a>
|
178
|
+
<a href="https://opencollective.com/n1_loader/backer/20/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/20/avatar.svg"></a>
|
179
|
+
<a href="https://opencollective.com/n1_loader/backer/21/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/21/avatar.svg"></a>
|
180
|
+
<a href="https://opencollective.com/n1_loader/backer/22/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/22/avatar.svg"></a>
|
181
|
+
<a href="https://opencollective.com/n1_loader/backer/23/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/23/avatar.svg"></a>
|
182
|
+
<a href="https://opencollective.com/n1_loader/backer/24/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/24/avatar.svg"></a>
|
183
|
+
<a href="https://opencollective.com/n1_loader/backer/25/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/25/avatar.svg"></a>
|
184
|
+
<a href="https://opencollective.com/n1_loader/backer/26/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/26/avatar.svg"></a>
|
185
|
+
<a href="https://opencollective.com/n1_loader/backer/27/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/27/avatar.svg"></a>
|
186
|
+
<a href="https://opencollective.com/n1_loader/backer/28/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/28/avatar.svg"></a>
|
187
|
+
<a href="https://opencollective.com/n1_loader/backer/29/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/29/avatar.svg"></a>
|
188
|
+
|
189
|
+
### Open Collective Sponsors
|
190
|
+
|
191
|
+
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)]
|
192
|
+
|
193
|
+
<a href="https://opencollective.com/n1_loader/sponsor/0/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/0/avatar.svg"></a>
|
194
|
+
<a href="https://opencollective.com/n1_loader/sponsor/1/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/1/avatar.svg"></a>
|
195
|
+
<a href="https://opencollective.com/n1_loader/sponsor/2/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/2/avatar.svg"></a>
|
196
|
+
<a href="https://opencollective.com/n1_loader/sponsor/3/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/3/avatar.svg"></a>
|
197
|
+
<a href="https://opencollective.com/n1_loader/sponsor/4/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/4/avatar.svg"></a>
|
198
|
+
<a href="https://opencollective.com/n1_loader/sponsor/5/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/5/avatar.svg"></a>
|
199
|
+
<a href="https://opencollective.com/n1_loader/sponsor/6/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/6/avatar.svg"></a>
|
200
|
+
<a href="https://opencollective.com/n1_loader/sponsor/7/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/7/avatar.svg"></a>
|
201
|
+
<a href="https://opencollective.com/n1_loader/sponsor/8/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/8/avatar.svg"></a>
|
202
|
+
<a href="https://opencollective.com/n1_loader/sponsor/9/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/9/avatar.svg"></a>
|
203
|
+
<a href="https://opencollective.com/n1_loader/sponsor/10/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/10/avatar.svg"></a>
|
204
|
+
<a href="https://opencollective.com/n1_loader/sponsor/11/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/11/avatar.svg"></a>
|
205
|
+
<a href="https://opencollective.com/n1_loader/sponsor/12/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/12/avatar.svg"></a>
|
206
|
+
<a href="https://opencollective.com/n1_loader/sponsor/13/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/13/avatar.svg"></a>
|
207
|
+
<a href="https://opencollective.com/n1_loader/sponsor/14/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/14/avatar.svg"></a>
|
208
|
+
<a href="https://opencollective.com/n1_loader/sponsor/15/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/15/avatar.svg"></a>
|
209
|
+
<a href="https://opencollective.com/n1_loader/sponsor/16/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/16/avatar.svg"></a>
|
210
|
+
<a href="https://opencollective.com/n1_loader/sponsor/17/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/17/avatar.svg"></a>
|
211
|
+
<a href="https://opencollective.com/n1_loader/sponsor/18/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/18/avatar.svg"></a>
|
212
|
+
<a href="https://opencollective.com/n1_loader/sponsor/19/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/19/avatar.svg"></a>
|
213
|
+
<a href="https://opencollective.com/n1_loader/sponsor/20/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/20/avatar.svg"></a>
|
214
|
+
<a href="https://opencollective.com/n1_loader/sponsor/21/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/21/avatar.svg"></a>
|
215
|
+
<a href="https://opencollective.com/n1_loader/sponsor/22/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/22/avatar.svg"></a>
|
216
|
+
<a href="https://opencollective.com/n1_loader/sponsor/23/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/23/avatar.svg"></a>
|
217
|
+
<a href="https://opencollective.com/n1_loader/sponsor/24/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/24/avatar.svg"></a>
|
218
|
+
<a href="https://opencollective.com/n1_loader/sponsor/25/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/25/avatar.svg"></a>
|
219
|
+
<a href="https://opencollective.com/n1_loader/sponsor/26/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/26/avatar.svg"></a>
|
220
|
+
<a href="https://opencollective.com/n1_loader/sponsor/27/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/27/avatar.svg"></a>
|
221
|
+
<a href="https://opencollective.com/n1_loader/sponsor/28/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/28/avatar.svg"></a>
|
222
|
+
<a href="https://opencollective.com/n1_loader/sponsor/29/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/29/avatar.svg"></a>
|
223
|
+
|
148
224
|
## Contributing
|
149
225
|
|
150
226
|
Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader.
|
@@ -173,4 +249,6 @@ Copyright (c) Evgeniy Demin. See [LICENSE.txt](LICENSE.txt) for further details.
|
|
173
249
|
[5]: https://github.com/rails/rails/tree/main/activerecord
|
174
250
|
[6]: https://github.com/DmitryTsepelev/ar_lazy_preload
|
175
251
|
[7]: https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping
|
176
|
-
[8]: https://github.com/djezzzl/n1_loader
|
252
|
+
[8]: https://github.com/djezzzl/n1_loader
|
253
|
+
[9]: https://opencollective.com/n1_loader/tiers/badge.svg
|
254
|
+
[10]: https://opencollective.com/n1_loader#support
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# Enhanced ActiveRecord
|
2
|
+
|
3
|
+
- Do you like `ActiveRecord` preloading?
|
4
|
+
- How many times have you resolved your N+1 issues with `includes` or `preload`?
|
5
|
+
- Do you know that preloading has limitations?
|
6
|
+
|
7
|
+
In this guide, I'd like to share with you tips and tricks about ActiveRecord
|
8
|
+
preloading and how you can enhance it to the next level.
|
9
|
+
|
10
|
+
Let's start by describing the models.
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# The model represents users in our application.
|
14
|
+
class User < ActiveRecord::Base
|
15
|
+
# Every user may have from 0 to many payments.
|
16
|
+
has_many :payments
|
17
|
+
end
|
18
|
+
|
19
|
+
# The model represents payments in our application.
|
20
|
+
class Payment < ActiveRecord::Base
|
21
|
+
# Every payment belongs to a user.
|
22
|
+
belongs_to :user
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
Assuming we want to iterate over a group of users and check how many payments they have, we may do:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
# The query we want to use to fetch users from the database.
|
30
|
+
users = User.all
|
31
|
+
# Iteration over selected users.
|
32
|
+
users.each do |user|
|
33
|
+
# Print amount of user's payments.
|
34
|
+
# This query will be called for every user, bringing an N+1 issue.
|
35
|
+
p user.payments.count
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
We can fix the N+1 issue above in a second.
|
40
|
+
We need to add ActiveRecord's `includes` to the query that fetches users.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# The query to fetch users with preload payments for every selected user.
|
44
|
+
users = User.includes(:payments).all
|
45
|
+
```
|
46
|
+
|
47
|
+
Then, we can iterate over the group again without the N+1 issue.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
users.each do |user|
|
51
|
+
p user.payments.count
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
Experienced with ActiveRecord person may notice that the iteration above still will have an N+1 issue.
|
56
|
+
The reason is the `.count` method and its behavior.
|
57
|
+
This issue brings us to the first tip.
|
58
|
+
|
59
|
+
### Tip 1. `count` vs `size` vs `length`
|
60
|
+
|
61
|
+
- `count` - always queries the database with `COUNT` query;
|
62
|
+
- `size` - queries the database with `COUNT` only when there is no preloaded data, returns array length otherwise;
|
63
|
+
- `length` - always returns array length, in case there is no data, load it first.
|
64
|
+
|
65
|
+
_Note:_ be careful with `size` as ordering is critical.
|
66
|
+
|
67
|
+
Meaning, for `user = User.first`
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# Does `COUNT` query
|
71
|
+
user.payments.size
|
72
|
+
# Does `SELECT` query
|
73
|
+
user.payments.each { |payment| }
|
74
|
+
```
|
75
|
+
|
76
|
+
is different from
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
# Does `SELECT` query
|
80
|
+
user.payments.each { |payment| }
|
81
|
+
# No query
|
82
|
+
user.payments.size
|
83
|
+
```
|
84
|
+
|
85
|
+
You may notice that the above solution loads all payment information when the amount is only needed.
|
86
|
+
There is a well-known solution for this case called [counter_cache](https://guides.rubyonrails.org/association_basics.html#options-for-belongs-to-counter-cache).
|
87
|
+
|
88
|
+
To use that, you need to add `payments_count` field to `users` table and adjust `Payment` model.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# Migration to add `payments_count` to `users` table.
|
92
|
+
class AddPaymentsCountToUsers < ActiveRecord::Migration
|
93
|
+
def change
|
94
|
+
add_column :users, :payments_count, :integer, default: 0, null: false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Change `belongs_to` to have `counter_cache` option.
|
99
|
+
class Payment < ActiveRecord::Base
|
100
|
+
belongs_to :user, counter_cache: true
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
_Note:_ avoid adding or removing payments from the database directly or through `insert_all`/`delete`/`delete_all` as
|
105
|
+
`counter_cache` is using ActiveRecord callbacks to update the field's value.
|
106
|
+
|
107
|
+
It's worth mentioning [counter_culture](https://github.com/magnusvk/counter_culture) alternative that has many features compared with the built-in `counter_cache`
|
108
|
+
|
109
|
+
## Associations with arguments
|
110
|
+
|
111
|
+
Now, let's assume we want to fetch the number of payments in a time frame for every user in a group.
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
from = 1.months.ago
|
115
|
+
to = Time.current
|
116
|
+
|
117
|
+
# Query to fetch users.
|
118
|
+
users = User.all
|
119
|
+
|
120
|
+
users.each do |user|
|
121
|
+
# Print the number of payments in a time frame for every user.
|
122
|
+
# Database query will be triggered for every user, meaning it has an N+1 issue.
|
123
|
+
p user.payments.where(created_at: from...to).count
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
ActiveRecord supports defining associations with arguments.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class User < ActiveRecord::Base
|
131
|
+
has_many :payments, -> (from, to) { where(created_at: from...to) }
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
Unfortunately, such associations are not possible to preload with `includes`.
|
136
|
+
Gladly, there is a solution with [N1Loader](https://github.com/djezzzl/n1_loader/).
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
# Install gem dependencies.
|
140
|
+
require 'n1_loader/active_record'
|
141
|
+
|
142
|
+
class User < ActiveRecord::Base
|
143
|
+
n1_optimized :payments_count do
|
144
|
+
argument :from
|
145
|
+
argument :to
|
146
|
+
|
147
|
+
def perform(users)
|
148
|
+
# Fetch the payment number once for all users.
|
149
|
+
payments = Payment.where(user: users).where(created_at: from...to).group(:user_id).count
|
150
|
+
|
151
|
+
users.each do |user|
|
152
|
+
# Assign preloaded data to every user.
|
153
|
+
# Note: it doesn't use any promises.
|
154
|
+
fulfill(user, payments[user.id])
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
from = 1.month.ago
|
161
|
+
to = Time.current
|
162
|
+
|
163
|
+
# Preload `payments` N1Loader "association". Doesn't query the database yet.
|
164
|
+
users = User.includes(:payments_count).all
|
165
|
+
|
166
|
+
users.each do |user|
|
167
|
+
# Queries the database once, meaning has no N+1 issues.
|
168
|
+
p user.payments_count(from, to)
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
Let's look at another example. Assuming we want to fetch the last payment for every user.
|
173
|
+
We can try to define scoped `has_one` association and use that.
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class User < ActiveRecord::Base
|
177
|
+
has_one :last_payment, -> { order(id: :desc) }, class_name: 'Payment'
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
We can see that preloading is working.
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
users = User.includes(:last_payment)
|
185
|
+
|
186
|
+
users.each do |user|
|
187
|
+
# No N+1. Last payment was returned.
|
188
|
+
p user.last_payment
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
At first glance, we may think everything is alright. Unfortunately, it is not.
|
193
|
+
|
194
|
+
### Tip 2. Enforce `has_one` associations on the database level
|
195
|
+
|
196
|
+
ActiveRecord, fetches all available payments for every user with provided order and then assigns only first payment to the association.
|
197
|
+
First, such querying is inefficient as we load many redundant information.
|
198
|
+
But most importantly, this association may lead to big issues. Other engineers may use it, for example,
|
199
|
+
for `joins(:last_payment)`. Assuming that association has strict agreement on the database level that
|
200
|
+
a user may have none or a single payment in the database. Apparently, it may not be the case, and some queries
|
201
|
+
will return unexpected data.
|
202
|
+
|
203
|
+
Described issues may be found with [DatabaseConsistency](https://github.com/djezzzl/database_consistency).
|
204
|
+
|
205
|
+
Back to the task, we can solve it with [N1Loader](https://github.com/djezzzl/n1_loader) in the following way
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
require 'n1_loader/active_record'
|
209
|
+
|
210
|
+
class User < ActiveRecord::Base
|
211
|
+
n1_optimized :last_payment do |users|
|
212
|
+
subquery = Payment.select('MAX(id)').where(user: users)
|
213
|
+
payments = Payment.where(id: subquery).index_by(&:user_id)
|
214
|
+
|
215
|
+
users.each do |user|
|
216
|
+
fulfill(user, payments[user.id])
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
users = User.includes(:last_payment).all
|
222
|
+
|
223
|
+
users.each do |user|
|
224
|
+
# Queries the database once, meaning no N+1.
|
225
|
+
p user.last_payment
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
Attentive reader could notice that in every described case, it was a requirement to explicitly list data that we want to preload for a group of users.
|
230
|
+
Gladly, there is a simple solution! [ArLazyPreload](https://github.com/DmitryTsepelev/ar_lazy_preload) will make N+1 disappear just by enabling it.
|
231
|
+
As soon as you need to load association for any record, it will load it once for all records that were fetched along this one.
|
232
|
+
And it works with ActiveRecord and N1Loader perfectly!
|
233
|
+
|
234
|
+
Let's look at the example.
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
# Require N1Loader with ArLazyPreload integration
|
238
|
+
require 'n1_loader/ar_lazy_preload'
|
239
|
+
|
240
|
+
# Enable ArLazyPreload globally, so you don't need to care about `includes` anymore
|
241
|
+
ArLazyPreload.config.auto_preload = true
|
242
|
+
|
243
|
+
class User < ActiveRecord::Base
|
244
|
+
has_many :payments
|
245
|
+
|
246
|
+
n1_optimized :last_payment do |users|
|
247
|
+
subquery = Payment.select('MAX(id)').where(user: users)
|
248
|
+
payments = Payment.where(id: subquery).index_by(&:user_id)
|
249
|
+
|
250
|
+
users.each do |user|
|
251
|
+
fulfill(user, payments[user.id])
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# no need to specify `includes`
|
257
|
+
users = User.all
|
258
|
+
|
259
|
+
users.each do |user|
|
260
|
+
p user.payments # no N+1
|
261
|
+
p user.last_payment # no N+1
|
262
|
+
end
|
263
|
+
```
|
264
|
+
|
265
|
+
As you can see, there is no need to even remember about resolving N+1 when you have both [ArLazyPreload](https://github.com/DmitryTsepelev/ar_lazy_preload) and [N1Loader](https://github.com/djezzzl/n1_loader) in your pocket.
|
266
|
+
It works great with GraphQL API too. Give it and try and share your feedback!
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module N1Loader
|
4
|
+
module ActiveRecord
|
5
|
+
module Associations
|
6
|
+
module Preloader # :nodoc:
|
7
|
+
N1LoaderReflection = Struct.new(:key, :loader) do
|
8
|
+
def options
|
9
|
+
{}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def preloaders_for_reflection(reflection, records)
|
14
|
+
return super unless reflection.is_a?(N1LoaderReflection)
|
15
|
+
|
16
|
+
N1Loader::Preloader.new(records).preload(reflection.key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def grouped_records # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
20
|
+
n1_load_records, records = source_records.partition do |record|
|
21
|
+
record.class.respond_to?(:n1_loader_defined?) && record.class.n1_loader_defined?(association)
|
22
|
+
end
|
23
|
+
|
24
|
+
h = n1_load_records.group_by do |record|
|
25
|
+
N1LoaderReflection.new(association, record.class.n1_loader(association))
|
26
|
+
end
|
27
|
+
|
28
|
+
polymorphic_parent = !root? && parent.polymorphic?
|
29
|
+
records.each do |record|
|
30
|
+
reflection = record.class._reflect_on_association(association)
|
31
|
+
next if polymorphic_parent && !reflection || !record.association(association).klass
|
32
|
+
|
33
|
+
(h[reflection] ||= []) << record
|
34
|
+
end
|
35
|
+
h
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -21,10 +21,14 @@ ActiveSupport.on_load(:active_record) do
|
|
21
21
|
case ActiveRecord::VERSION::MAJOR
|
22
22
|
when 6
|
23
23
|
require_relative "active_record/associations_preloader_v6"
|
24
|
-
|
24
|
+
ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
|
25
|
+
when 5
|
25
26
|
require_relative "active_record/associations_preloader_v5"
|
27
|
+
ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
|
28
|
+
else
|
29
|
+
require_relative "active_record/associations_preloader_v7"
|
30
|
+
ActiveRecord::Associations::Preloader::Branch.prepend(N1Loader::ActiveRecord::Associations::Preloader)
|
26
31
|
end
|
27
32
|
|
28
|
-
ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
|
29
33
|
ActiveRecord::Base.include(N1Loader::ActiveRecord::Base)
|
30
34
|
end
|
data/lib/n1_loader/version.rb
CHANGED
data/n1_loader.gemspec
CHANGED
@@ -14,17 +14,17 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.required_ruby_version = ">= 2.5.0"
|
15
15
|
|
16
16
|
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
-
spec.metadata["source_code_uri"] = "https://github.com/djezzzl/
|
18
|
-
spec.metadata["changelog_uri"] = "https://github.com/djezzzl/
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/djezzzl/n1_loader"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/djezzzl/n1_loader/master/CHANGELOG.md"
|
19
19
|
|
20
20
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
21
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
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"
|
26
26
|
spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
|
27
|
-
spec.add_development_dependency "db-query-matchers", "~> 0.
|
27
|
+
spec.add_development_dependency "db-query-matchers", "~> 0.11"
|
28
28
|
spec.add_development_dependency "graphql", "~> 2.0"
|
29
29
|
spec.add_development_dependency "rails", ">= 5"
|
30
30
|
spec.add_development_dependency "rspec", "~> 3.0"
|
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
|
+
version: 1.5.1
|
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-
|
11
|
+
date: 2022-09-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -17,9 +17,6 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '5'
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '7'
|
23
20
|
type: :development
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -27,9 +24,6 @@ dependencies:
|
|
27
24
|
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '5'
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '7'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: ar_lazy_preload
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -50,14 +44,14 @@ dependencies:
|
|
50
44
|
requirements:
|
51
45
|
- - "~>"
|
52
46
|
- !ruby/object:Gem::Version
|
53
|
-
version: '0.
|
47
|
+
version: '0.11'
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
51
|
requirements:
|
58
52
|
- - "~>"
|
59
53
|
- !ruby/object:Gem::Version
|
60
|
-
version: '0.
|
54
|
+
version: '0.11'
|
61
55
|
- !ruby/object:Gem::Dependency
|
62
56
|
name: graphql
|
63
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -161,6 +155,7 @@ files:
|
|
161
155
|
- Rakefile
|
162
156
|
- activerecord-gemfiles/ar_5_latest.gemfile
|
163
157
|
- activerecord-gemfiles/ar_6_latest.gemfile
|
158
|
+
- activerecord-gemfiles/ar_7_latest.gemfile
|
164
159
|
- ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile
|
165
160
|
- ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile
|
166
161
|
- bin/console
|
@@ -178,10 +173,12 @@ files:
|
|
178
173
|
- examples/reloading.rb
|
179
174
|
- examples/shared_loader.rb
|
180
175
|
- examples/single_case.rb
|
176
|
+
- guides/enhanced-activerecord.md
|
181
177
|
- lib/n1_loader.rb
|
182
178
|
- lib/n1_loader/active_record.rb
|
183
179
|
- lib/n1_loader/active_record/associations_preloader_v5.rb
|
184
180
|
- lib/n1_loader/active_record/associations_preloader_v6.rb
|
181
|
+
- lib/n1_loader/active_record/associations_preloader_v7.rb
|
185
182
|
- lib/n1_loader/active_record/base.rb
|
186
183
|
- lib/n1_loader/active_record/loader.rb
|
187
184
|
- lib/n1_loader/active_record/loader_collection.rb
|
@@ -203,8 +200,8 @@ licenses:
|
|
203
200
|
- MIT
|
204
201
|
metadata:
|
205
202
|
homepage_uri: https://github.com/djezzzl/n1_loader
|
206
|
-
source_code_uri: https://github.com/djezzzl/
|
207
|
-
changelog_uri: https://github.com/djezzzl/
|
203
|
+
source_code_uri: https://github.com/djezzzl/n1_loader
|
204
|
+
changelog_uri: https://github.com/djezzzl/n1_loader/master/CHANGELOG.md
|
208
205
|
post_install_message:
|
209
206
|
rdoc_options: []
|
210
207
|
require_paths:
|
@@ -220,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
220
217
|
- !ruby/object:Gem::Version
|
221
218
|
version: '0'
|
222
219
|
requirements: []
|
223
|
-
rubygems_version: 3.
|
220
|
+
rubygems_version: 3.2.22
|
224
221
|
signing_key:
|
225
222
|
specification_version: 4
|
226
223
|
summary: Loader to solve N+1 issue for good.
|