N1Loader
N1Loader is designed to provide a simple way for avoiding N+1 issues of any kind. For example, it can help with resolving N+1 for:
- database querying (most common case)
- 3rd party service calls
- complex calculations
- and many more
If the project helps you or your organization, I would be very grateful if you contribute or donate.
Your support is an incredible motivation and the biggest reward for my hard work.
Support: ActiveRecord 5, 6, and 7.
Follow me and stay tuned for the updates:
Killer feature for GraphQL API
N1Loader in combination with ArLazyPreload is a killer feature for your GraphQL API. Give it a try now and see incredible results instantly! Check out the example and start benefiting from it in your projects!
gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
Enhance ActiveRecord
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! Check out the detailed guide with examples or its short version.
gem 'n1_loader', require: 'n1_loader/active_record'
Are you ready to forget about N+1 once and for all? Install ArLazyPreload and see dreams come true!
gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
Standalone mode
Are you not working with ActiveRecord? N1Loader is ready to be used as standalone solution! (full snippet)
gem 'n1_loader'
How to use it?
N1Loader provides DSL that allows you to define N+1 ready loaders that can be injected into your objects in a way that you can avoid N+1 issues.
Disclaimer: examples below are working but designed to show N1Loader potentials only. In real live applications, N1Loader can be applied anywhere and in more elegant way.
Let's look at simple example below (full snippet):
class User < ActiveRecord::Base
has_many :payments
n1_optimized :payments_total do |users|
total_per_user =
Payment.group(:user_id)
.where(user: users)
.sum(:amount)
.tap { |h| h.default = 0 }
users.each do |user|
total = total_per_user[user.id]
fulfill(user, total)
end
end
end
class Payment < ActiveRecord::Base
belongs_to :user
validates :amount, presence: true
end
# A user has many payments.
# Assuming, we want to know for group of users, what is a total of their payments, we can do the following:
# Has N+1 issue
p User.all.map { |user| user.payments.sum(&:amount) }
# Has no N+1 but we load too many data that we don't actually need
p User.all.includes(:payments).map { |user| user.payments.sum(&:amount) }
# Has no N+1 and we load only what we need
p User.all.includes(:payments_total).map { |user| user.payments_total }
Let's assume now, that we want to calculate the total of payments for the given period for a group of users. N1Loader can do that as well! (full snippet)
class User < ActiveRecord::Base
has_many :payments
n1_optimized :payments_total do
argument :from
argument :to
def perform(users)
total_per_user =
Payment
.group(:user_id)
.where(created_at: from..to)
.where(user: users)
.sum(:amount)
.tap { |h| h.default = 0 }
users.each do |user|
total = total_per_user[user.id]
fulfill(user, total)
end
end
end
end
class Payment < ActiveRecord::Base
belongs_to :user
validates :amount, presence: true
end
# Has N+1
p User.all.map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }
# Has no N+1 but we load too many data that we don't need
p User.all.includes(:payments).map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) }
# Has no N+1 and calculation is the most efficient
p User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) }
Features and benefits
- N1Loader doesn't use Promises which means it's easy to debug
- Doesn't require injection to objects, can be used in isolation
- Loads data lazily
- Loaders can be shared between multiple classes
- Loaded data can be re-fetched
- Loader can be optimized for single cases
- Loader support arguments
- Has integration with ActiveRecord which makes it brilliant
- Has integration with ArLazyPreload which makes it excellent
Feature killer for ArLazyPreload integration with isolated loaders
In version 1.6.0 isolated loaders were integrated with ArLazyPreload context.
This means, it isn't required to inject N1Loader
into your ActiveRecord models to avoid N+1 issues out of the box.
It is especially great as many engineers are trying to avoid extra coupling between their models/services when it's possible.
And this feature was designed exactly for this without losing an out of a box solution for N+1.
Without further ado, please have a look at the example.
Spoiler: as soon as you have your loader defined, it will be as simple as Loader.for(element)
to get your data efficiently and without N+1.
Funding
Open Collective Backers
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]
Open Collective Sponsors
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]
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the N1Loader project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
Changelog
N1Loader's changelog is available here.
Copyright
Copyright (c) Evgeniy Demin. See LICENSE.txt for further details.