n1_loader 1.7.1 → 1.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/n1_loader/active_record/associations_preloader_v5.rb +2 -2
- data/lib/n1_loader/active_record/associations_preloader_v6.rb +2 -2
- data/lib/n1_loader/active_record/associations_preloader_v7.rb +2 -2
- data/lib/n1_loader/ar_lazy_preload/loadable.rb +5 -14
- data/lib/n1_loader/ar_lazy_preload.rb +1 -1
- data/lib/n1_loader/core/loadable.rb +16 -59
- data/lib/n1_loader/core/loader.rb +8 -5
- data/lib/n1_loader/core/loader_builder.rb +16 -0
- data/lib/n1_loader/core/preloader.rb +5 -8
- data/lib/n1_loader/version.rb +1 -1
- data/lib/n1_loader.rb +1 -1
- metadata +3 -37
- data/.github/workflows/rubocop.yml +0 -24
- data/.github/workflows/tests.yml +0 -66
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/.rubocop.yml +0 -20
- data/CHANGELOG.md +0 -129
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -15
- data/LICENSE.txt +0 -21
- data/README.md +0 -276
- data/Rakefile +0 -12
- data/activerecord-gemfiles/ar_5_latest.gemfile +0 -3
- data/activerecord-gemfiles/ar_6_latest.gemfile +0 -3
- data/activerecord-gemfiles/ar_7_latest.gemfile +0 -3
- data/ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile +0 -3
- data/ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile +0 -3
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/examples/active_record_integration.rb +0 -33
- data/examples/ar_lazy_integration.rb +0 -36
- data/examples/ar_lazy_integration_with_isolated_loader.rb +0 -39
- data/examples/arguments_support.rb +0 -67
- data/examples/context/service.rb +0 -20
- data/examples/context/setup_ar_lazy.rb +0 -15
- data/examples/context/setup_database.rb +0 -26
- data/examples/core.rb +0 -39
- data/examples/graphql.rb +0 -63
- data/examples/isolated_loader.rb +0 -13
- data/examples/lazy_loading.rb +0 -26
- data/examples/reloading.rb +0 -32
- data/examples/shared_loader.rb +0 -34
- data/examples/single_case.rb +0 -34
- data/guides/enhanced-activerecord.md +0 -266
- data/lib/n1_loader/core/name.rb +0 -14
- data/n1_loader.gemspec +0 -35
data/examples/context/service.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
# 3rd party service, or database, or anything else that can perform in batches
|
2
|
-
class Service
|
3
|
-
def self.count
|
4
|
-
@count ||= 0
|
5
|
-
end
|
6
|
-
|
7
|
-
def self.increase!
|
8
|
-
@count = (@count || 0) + 1
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.receive(*users)
|
12
|
-
increase!
|
13
|
-
|
14
|
-
users.flatten.map(&:object_id)
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.single(user)
|
18
|
-
user.object_id
|
19
|
-
end
|
20
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
ActiveSupport.on_load(:active_record) do
|
2
|
-
ActiveRecord::Base.include(ArLazyPreload::Base)
|
3
|
-
|
4
|
-
ActiveRecord::Relation.prepend(ArLazyPreload::Relation)
|
5
|
-
ActiveRecord::AssociationRelation.prepend(ArLazyPreload::AssociationRelation)
|
6
|
-
ActiveRecord::Relation::Merger.prepend(ArLazyPreload::Merger)
|
7
|
-
|
8
|
-
[
|
9
|
-
ActiveRecord::Associations::CollectionAssociation,
|
10
|
-
ActiveRecord::Associations::Association
|
11
|
-
].each { |klass| klass.prepend(ArLazyPreload::Association) }
|
12
|
-
|
13
|
-
ActiveRecord::Associations::CollectionAssociation.prepend(ArLazyPreload::CollectionAssociation)
|
14
|
-
ActiveRecord::Associations::CollectionProxy.prepend(ArLazyPreload::CollectionProxy)
|
15
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require "sqlite3"
|
2
|
-
|
3
|
-
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
4
|
-
ActiveRecord::Base.connection.tables.each do |table|
|
5
|
-
ActiveRecord::Base.connection.drop_table(table, force: :cascade)
|
6
|
-
end
|
7
|
-
ActiveRecord::Schema.verbose = false
|
8
|
-
ActiveRecord::Base.logger = Logger.new($stdout)
|
9
|
-
|
10
|
-
ActiveRecord::Schema.define(version: 1) do
|
11
|
-
create_table(:payments) do |t|
|
12
|
-
t.belongs_to :user
|
13
|
-
t.integer :amount
|
14
|
-
t.timestamps
|
15
|
-
end
|
16
|
-
create_table(:users)
|
17
|
-
end
|
18
|
-
|
19
|
-
def fill_database
|
20
|
-
10.times do
|
21
|
-
user = User.create!
|
22
|
-
10.times do
|
23
|
-
Payment.create!(user: user, amount: rand(1000))
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
data/examples/core.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "n1_loader"
|
4
|
-
|
5
|
-
require_relative 'context/service'
|
6
|
-
|
7
|
-
# Class that wants to request 3rd party service without N+1
|
8
|
-
class User
|
9
|
-
include N1Loader::Loadable
|
10
|
-
|
11
|
-
def unoptimized_call
|
12
|
-
Service.receive(self)[0]
|
13
|
-
end
|
14
|
-
|
15
|
-
n1_optimized :optimized_call do |users|
|
16
|
-
data = Service.receive(users)
|
17
|
-
|
18
|
-
users.each_with_index do |user, index|
|
19
|
-
fulfill(user, data[index])
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# works fine for single case
|
25
|
-
user = User.new
|
26
|
-
p "Works correctly: #{user.unoptimized_call == user.optimized_call}"
|
27
|
-
|
28
|
-
users = [User.new, User.new]
|
29
|
-
|
30
|
-
# Has N+1
|
31
|
-
count_before = Service.count
|
32
|
-
p users.map(&:unoptimized_call)
|
33
|
-
p "Has N+1 #{Service.count == count_before + users.count}"
|
34
|
-
|
35
|
-
# Has no N+1
|
36
|
-
count_before = Service.count
|
37
|
-
N1Loader::Preloader.new(users).preload(:optimized_call)
|
38
|
-
p users.map(&:optimized_call)
|
39
|
-
p "Has no N+1: #{Service.count == count_before + 1}"
|
data/examples/graphql.rb
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "n1_loader/ar_lazy_preload"
|
4
|
-
require 'graphql'
|
5
|
-
|
6
|
-
require_relative 'context/setup_database'
|
7
|
-
require_relative 'context/setup_ar_lazy'
|
8
|
-
|
9
|
-
class User < ActiveRecord::Base
|
10
|
-
has_many :payments
|
11
|
-
|
12
|
-
n1_optimized :payments_total do |users|
|
13
|
-
total_per_user = Payment.group(:user_id).where(user: users).sum(:amount).tap { |h| h.default = 0 }
|
14
|
-
|
15
|
-
users.each do |user|
|
16
|
-
total = total_per_user[user.id]
|
17
|
-
fulfill(user, total)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
class Payment < ActiveRecord::Base
|
23
|
-
belongs_to :user
|
24
|
-
|
25
|
-
validates :amount, presence: true
|
26
|
-
end
|
27
|
-
|
28
|
-
10.times do
|
29
|
-
user = User.create!
|
30
|
-
10.times do
|
31
|
-
Payment.create!(user: user, amount: rand(1000))
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
ArLazyPreload.config.auto_preload = true
|
36
|
-
# Or use +preload_associations_lazily+ when loading objects from database
|
37
|
-
|
38
|
-
class UserType < GraphQL::Schema::Object
|
39
|
-
field :payments_total, Integer
|
40
|
-
end
|
41
|
-
|
42
|
-
class QueryType < GraphQL::Schema::Object
|
43
|
-
field :users, [UserType]
|
44
|
-
|
45
|
-
def users
|
46
|
-
User.all
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
class Schema < GraphQL::Schema
|
51
|
-
query QueryType
|
52
|
-
end
|
53
|
-
|
54
|
-
query_string = <<~GQL
|
55
|
-
{
|
56
|
-
users {
|
57
|
-
paymentsTotal
|
58
|
-
}
|
59
|
-
}
|
60
|
-
GQL
|
61
|
-
|
62
|
-
# No N+1. And never will be!
|
63
|
-
p Schema.execute(query_string)['data']
|
data/examples/isolated_loader.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
require 'n1_loader'
|
2
|
-
|
3
|
-
class IsolatedLoader < N1Loader::Loader
|
4
|
-
def perform(elements)
|
5
|
-
elements.each { |element| fulfill(element, [element]) }
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
objects = [1, 2, 3, 4]
|
10
|
-
loader = IsolatedLoader.new(objects)
|
11
|
-
objects.each do |object|
|
12
|
-
loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
|
13
|
-
end
|
data/examples/lazy_loading.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'n1_loader'
|
2
|
-
|
3
|
-
require_relative 'context/service'
|
4
|
-
|
5
|
-
# Class that wants to request 3rd party service without N+1
|
6
|
-
class User
|
7
|
-
include N1Loader::Loadable
|
8
|
-
|
9
|
-
n1_optimized :optimized_call do |users|
|
10
|
-
data = Service.receive(users)
|
11
|
-
|
12
|
-
users.each_with_index do |user, index|
|
13
|
-
fulfill(user, data[index])
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
users = [User.new, User.new, User.new]
|
19
|
-
|
20
|
-
# Initialized loader but didn't perform it yet
|
21
|
-
N1Loader::Preloader.new(users).preload(:optimized_call)
|
22
|
-
p "No calls yet: #{Service.count == 0}"
|
23
|
-
|
24
|
-
# First time loading
|
25
|
-
users.map(&:optimized_call)
|
26
|
-
p "First time loaded: #{Service.count == 1}"
|
data/examples/reloading.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
require 'n1_loader'
|
2
|
-
|
3
|
-
require_relative 'context/service'
|
4
|
-
|
5
|
-
class User
|
6
|
-
include N1Loader::Loadable
|
7
|
-
|
8
|
-
n1_optimized :optimized_call do |users|
|
9
|
-
data = Service.receive(users)
|
10
|
-
|
11
|
-
users.each_with_index do |user, index|
|
12
|
-
fulfill(user, data[index])
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
users = [User.new, User.new, User.new]
|
18
|
-
|
19
|
-
# Initialized loader but didn't perform it yet
|
20
|
-
N1Loader::Preloader.new(users).preload(:optimized_call)
|
21
|
-
p "No calls yet: #{Service.count == 0}"
|
22
|
-
|
23
|
-
# First time loading
|
24
|
-
users.map(&:optimized_call)
|
25
|
-
p "First time loaded: #{Service.count == 1}"
|
26
|
-
|
27
|
-
users.first.optimized_call(reload: true)
|
28
|
-
p "Reloaded for this object only: #{Service.count == 2}"
|
29
|
-
|
30
|
-
users.first.n1_clear_cache
|
31
|
-
users.first.optimized_call
|
32
|
-
p "Reloaded for this object only: #{Service.count == 3}"
|
data/examples/shared_loader.rb
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
require 'n1_loader'
|
2
|
-
|
3
|
-
require_relative 'context/service'
|
4
|
-
|
5
|
-
# Loader that will be shared between multiple classes
|
6
|
-
class SharedLoader < N1Loader::Loader
|
7
|
-
def perform(objects)
|
8
|
-
data = Service.receive(objects)
|
9
|
-
|
10
|
-
objects.each_with_index do |user, index|
|
11
|
-
fulfill(user, data[index])
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
class User
|
17
|
-
include N1Loader::Loadable
|
18
|
-
|
19
|
-
n1_optimized :optimized_call, SharedLoader
|
20
|
-
end
|
21
|
-
|
22
|
-
class Payment
|
23
|
-
include N1Loader::Loadable
|
24
|
-
|
25
|
-
n1_optimized :optimized_call, SharedLoader
|
26
|
-
end
|
27
|
-
|
28
|
-
objects = [User.new, Payment.new, User.new, Payment.new]
|
29
|
-
|
30
|
-
N1Loader::Preloader.new(objects).preload(:optimized_call)
|
31
|
-
|
32
|
-
# First time loading for all objects
|
33
|
-
objects.map(&:optimized_call)
|
34
|
-
p "Loaded for all once: #{Service.count == 1}"
|
data/examples/single_case.rb
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
require 'n1_loader'
|
2
|
-
|
3
|
-
require_relative 'context/service'
|
4
|
-
|
5
|
-
# Loader that will be shared between multiple classes
|
6
|
-
class OptimizedLoader < N1Loader::Loader
|
7
|
-
def perform(objects)
|
8
|
-
data = Service.receive(objects)
|
9
|
-
|
10
|
-
objects.each_with_index do |user, index|
|
11
|
-
fulfill(user, data[index])
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def single(object)
|
16
|
-
Service.single(object)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
class User
|
21
|
-
include N1Loader::Loadable
|
22
|
-
|
23
|
-
n1_optimized :optimized_call, OptimizedLoader
|
24
|
-
end
|
25
|
-
|
26
|
-
objects = [User.new, User.new]
|
27
|
-
|
28
|
-
N1Loader::Preloader.new(objects).preload(:optimized_call)
|
29
|
-
|
30
|
-
objects.map(&:optimized_call)
|
31
|
-
p "Used multi-case perform: #{Service.count == 1}"
|
32
|
-
|
33
|
-
User.new.optimized_call
|
34
|
-
p "Used single-case perform: #{Service.count == 1}"
|
@@ -1,266 +0,0 @@
|
|
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!
|
data/lib/n1_loader/core/name.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module N1Loader
|
4
|
-
# Add support of question mark names
|
5
|
-
module Name
|
6
|
-
def n1_loader_name(name)
|
7
|
-
to_sym = name.is_a?(Symbol)
|
8
|
-
|
9
|
-
converted = name.to_s.gsub("?", "_question_mark")
|
10
|
-
|
11
|
-
to_sym ? converted.to_sym : converted
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
data/n1_loader.gemspec
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "lib/n1_loader/version"
|
4
|
-
|
5
|
-
Gem::Specification.new do |spec|
|
6
|
-
spec.name = "n1_loader"
|
7
|
-
spec.version = N1Loader::VERSION
|
8
|
-
spec.authors = ["Evgeniy Demin"]
|
9
|
-
spec.email = ["lawliet.djez@gmail.com"]
|
10
|
-
|
11
|
-
spec.summary = "Loader to solve N+1 issue for good."
|
12
|
-
spec.homepage = "https://github.com/djezzzl/n1_loader"
|
13
|
-
spec.license = "MIT"
|
14
|
-
spec.required_ruby_version = ">= 2.5.0"
|
15
|
-
|
16
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
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
|
-
spec.metadata["funding_uri"] = "https://opencollective.com/n1_loader#support"
|
20
|
-
|
21
|
-
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
23
|
-
end
|
24
|
-
spec.require_paths = ["lib"]
|
25
|
-
|
26
|
-
spec.add_development_dependency "activerecord", ">= 5"
|
27
|
-
spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
|
28
|
-
spec.add_development_dependency "db-query-matchers", "~> 0.11"
|
29
|
-
spec.add_development_dependency "graphql", "~> 2.0"
|
30
|
-
spec.add_development_dependency "rails", ">= 5"
|
31
|
-
spec.add_development_dependency "rspec", "~> 3.0"
|
32
|
-
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
|
33
|
-
spec.add_development_dependency "rubocop", "~> 1.7"
|
34
|
-
spec.add_development_dependency "sqlite3", "~> 1.3"
|
35
|
-
end
|