n1_loader 1.5.0 → 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: bf26363e90cf5f0b6759776698445183f4abe8442bf366e7109e1cada18b1eaa
4
- data.tar.gz: e082a72dc1f910262fbc73040b484ae28e437beee866ca42277a7556d989072a
3
+ metadata.gz: eef4b4cb71bedafe0820631695d26d1782f11d940899c8cdd2f745179197e489
4
+ data.tar.gz: 909035eceab471c23057fc075a719f22ab8d2138c114c92500153cc19178f8c0
5
5
  SHA512:
6
- metadata.gz: 52993103afc0075d9d71088e6d7e60925ea1740176ee31829acb5d42009a38955b25d8e9124a17fff981b4735f443c04d3532cbea3186850bffc2908f0e40522
7
- data.tar.gz: fdcc21a626ff199ccf4a9af7bd860516745693614404c44159414b27601750ea476de8eae593f7bade3782425b638f784db096e6dcdaafa58c71382f3a08b979
6
+ metadata.gz: 5a47920e7df24aefbd413b8c2f2fdd6fd4e5918481109b8b79af01070d9c0f262784df0410ad6e758753405acaa7ffcc6a9dd2fc22eeab51cd5377885473b842
7
+ data.tar.gz: 8139a30fc4b15740c403aeee3791e56a4268d1890d0cb4aedd4d9d29e9b54c26a1e98541b2865932a005abf7d854b7461c0de4c3322beb7e08124f97fb75a11c
data/.circleci/config.yml CHANGED
@@ -100,7 +100,6 @@ workflows:
100
100
  matrix:
101
101
  parameters:
102
102
  ruby-version: [
103
- "2.5",
104
103
  "2.7",
105
104
  "latest"
106
105
  ]
@@ -114,19 +113,6 @@ workflows:
114
113
  "ar_lazy_preload_master"
115
114
  ]
116
115
  exclude:
117
- # Ruby 2.5 and AR Lazy Preload 1+
118
- - ruby-version: "2.5"
119
- ar_lazy_preload-gemfile: "ar_lazy_preload_master"
120
- activerecord-gemfile: "ar_5_latest"
121
-
122
- - ruby-version: "2.5"
123
- ar_lazy_preload-gemfile: "ar_lazy_preload_master"
124
- activerecord-gemfile: "ar_6_latest"
125
-
126
- - ruby-version: "2.5"
127
- ar_lazy_preload-gemfile: "ar_lazy_preload_master"
128
- activerecord-gemfile: "ar_7_latest"
129
-
130
116
  # AR 5 and ruby 3+
131
117
  - ruby-version: "latest"
132
118
  activerecord-gemfile: "ar_5_latest"
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
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
+
1
5
  ## [1.5.0] - 2022/05/01
2
6
 
3
7
  - Add support of Rails 7
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![CircleCI][1]][2]
4
4
  [![Gem Version][3]][4]
5
+ [![][9]][10]
5
6
 
6
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:
@@ -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.
@@ -24,6 +27,7 @@ gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
24
27
  ## Enhance [ActiveRecord][5]
25
28
 
26
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.
@@ -174,3 +250,5 @@ Copyright (c) Evgeniy Demin. See [LICENSE.txt](LICENSE.txt) for further details.
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
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!
@@ -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.5.0"
4
+ VERSION = "1.5.1"
5
5
  end
data/n1_loader.gemspec CHANGED
@@ -14,8 +14,8 @@ 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)/}) }
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.5.0
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-05-01 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
@@ -173,6 +173,7 @@ files:
173
173
  - examples/reloading.rb
174
174
  - examples/shared_loader.rb
175
175
  - examples/single_case.rb
176
+ - guides/enhanced-activerecord.md
176
177
  - lib/n1_loader.rb
177
178
  - lib/n1_loader/active_record.rb
178
179
  - lib/n1_loader/active_record/associations_preloader_v5.rb
@@ -199,8 +200,8 @@ licenses:
199
200
  - MIT
200
201
  metadata:
201
202
  homepage_uri: https://github.com/djezzzl/n1_loader
202
- source_code_uri: https://github.com/djezzzl/database_consistency
203
- 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
204
205
  post_install_message:
205
206
  rdoc_options: []
206
207
  require_paths:
@@ -216,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
216
217
  - !ruby/object:Gem::Version
217
218
  version: '0'
218
219
  requirements: []
219
- rubygems_version: 3.1.6
220
+ rubygems_version: 3.2.22
220
221
  signing_key:
221
222
  specification_version: 4
222
223
  summary: Loader to solve N+1 issue for good.