n1_loader 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.