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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e0025899b1fbd0329159dcd2e1ddf1d28318fe76354fd2e1378b803e49a9d96
4
- data.tar.gz: 2f0f222cd9298bb56e2c3a9ae8c1f6559c7ec2bec3653c5a9a2c7c172b49b7fd
3
+ metadata.gz: eef4b4cb71bedafe0820631695d26d1782f11d940899c8cdd2f745179197e489
4
+ data.tar.gz: 909035eceab471c23057fc075a719f22ab8d2138c114c92500153cc19178f8c0
5
5
  SHA512:
6
- metadata.gz: 48a55dd01386410a58a5fbf8ea9d556ac6fa2db33411a469b2c9f3fc99b9ccec2ad36eb482e0adba39a84af4ffd4adba6a51a8db23eba7869c03aca51add1d39
7
- data.tar.gz: b4c275204251a8e6908e34af67beacd6bbc05a1fcd1168350ab538ecf48362c840c671d1223dfd3659b2fe87f1428ee0e012144c3c52fa11451a595a0b5a11d9
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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem "activerecord", "~> 7"
@@ -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
@@ -5,3 +5,11 @@ N1Loader::LoaderCollection.define_method :preloaded_records do
5
5
 
6
6
  with.preloaded_records
7
7
  end
8
+
9
+ N1Loader::LoaderCollection.define_method :runnable_loaders do
10
+ [self]
11
+ end
12
+
13
+ N1Loader::LoaderCollection.define_method :run? do
14
+ true
15
+ 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
- else
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
@@ -23,7 +23,9 @@ module N1Loader
23
23
 
24
24
  @arguments ||= []
25
25
 
26
- define_method(name) { args[name] ||= opts[:default]&.call }
26
+ define_method(name) do
27
+ args.fetch(name) { args[name] = opts[:default]&.call }
28
+ end
27
29
 
28
30
  @arguments << opts.merge(name: name)
29
31
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module N1Loader
4
- VERSION = "1.4.4"
4
+ VERSION = "1.5.1"
5
5
  end
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/database_consistency"
18
- spec.metadata["changelog_uri"] = "https://github.com/djezzzl/database_consistency/master/CHANGELOG.md"
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", "< 7"
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.10"
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.4
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-04-29 00:00:00.000000000 Z
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.10'
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.10'
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/database_consistency
207
- changelog_uri: https://github.com/djezzzl/database_consistency/master/CHANGELOG.md
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.1.6
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.