n1_loader

Loader to solve N+1 issues for good. Highly recommended for GraphQL API.

224
5
Ruby

N1Loader

Gem Version


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

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 © Evgeniy Demin. See LICENSE.txt for further details.