Repository: djezzzl/n1_loader
Branch: master
Commit: f54f236b22e8
Files: 79
Total size: 126.5 KB
Directory structure:
gitextract_b233zkyp/
├── .github/
│ ├── FUNDING.yml
│ ├── copilot-instructions.md
│ └── workflows/
│ ├── copilot-setup-steps.yml
│ ├── release.yml
│ ├── rubocop.yml
│ └── tests.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── activerecord-gemfiles/
│ ├── ar_5_latest.gemfile
│ ├── ar_6_latest.gemfile
│ ├── ar_7_latest.gemfile
│ └── ar_8_latest.gemfile
├── ar_lazy_preload-gemfiles/
│ ├── ar_lazy_preload_0.6.1.gemfile
│ └── ar_lazy_preload_master.gemfile
├── bin/
│ ├── console
│ └── setup
├── examples/
│ ├── active_record_integration.rb
│ ├── ar_lazy_integration.rb
│ ├── ar_lazy_integration_with_isolated_loader.rb
│ ├── arguments_support.rb
│ ├── context/
│ │ ├── service.rb
│ │ ├── setup_ar_lazy.rb
│ │ └── setup_database.rb
│ ├── core.rb
│ ├── goldiloader_integration.rb
│ ├── goldiloader_integration_with_isolated_loader.rb
│ ├── graphql.rb
│ ├── isolated_loader.rb
│ ├── lazy_loading.rb
│ ├── n1_bind_to.rb
│ ├── reloading.rb
│ ├── shared_loader.rb
│ └── single_case.rb
├── goldiloader-gemfiles/
│ └── goldiloader.gemfile
├── guides/
│ └── enhanced-activerecord.md
├── lib/
│ ├── n1_loader/
│ │ ├── active_record/
│ │ │ ├── associations_preloader.rb
│ │ │ ├── associations_preloader_v5.rb
│ │ │ ├── associations_preloader_v6.rb
│ │ │ ├── base.rb
│ │ │ ├── loader.rb
│ │ │ └── loader_collection.rb
│ │ ├── active_record.rb
│ │ ├── ar_lazy_preload/
│ │ │ ├── associated_context_builder.rb
│ │ │ ├── context.rb
│ │ │ ├── context_adapter.rb
│ │ │ ├── loadable.rb
│ │ │ ├── loader.rb
│ │ │ ├── loader_collection_patch.rb
│ │ │ ├── loader_patch.rb
│ │ │ └── preloader_patch.rb
│ │ ├── ar_lazy_preload.rb
│ │ ├── core/
│ │ │ ├── loadable.rb
│ │ │ ├── loader.rb
│ │ │ ├── loader_builder.rb
│ │ │ ├── loader_collection.rb
│ │ │ └── preloader.rb
│ │ ├── goldiloader/
│ │ │ ├── context.rb
│ │ │ ├── context_adapter.rb
│ │ │ ├── loadable.rb
│ │ │ ├── loader.rb
│ │ │ ├── loader_collection_patch.rb
│ │ │ ├── loader_patch.rb
│ │ │ └── preloader_patch.rb
│ │ ├── goldiloader.rb
│ │ └── version.rb
│ └── n1_loader.rb
├── n1_loader.gemspec
└── spec/
├── activerecord_spec.rb
├── ar_lazy_preload_spec.rb
├── goldiloader_spec.rb
├── n1_loader_spec.rb
└── spec_helper.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# Supported platforms for funding
open_collective: n1_loader
github: djezzzl
================================================
FILE: .github/copilot-instructions.md
================================================
# Copilot Instructions
Every PR must have no errors from:
```
bundle exec rubocop
bundle exec rspec spec/n1_loader_spec.rb
bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb
bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb
```
================================================
FILE: .github/workflows/copilot-setup-steps.yml
================================================
name: Copilot Setup Steps
# Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
env:
ACTIVERECORD_GEMFILE: 'ar_8_latest'
AR_LAZY_PRELOAD_GEMFILE: 'ar_lazy_preload_master'
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: head
- name: Install dependencies
run: bundle install
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g. 2.1.4)'
required: true
jobs:
release:
runs-on: ubuntu-latest
if: github.actor == 'djezzzl'
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Install dependencies
run: bundle install
- name: Validate version format
run: |
if ! echo "${{ github.event.inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Invalid version format: ${{ github.event.inputs.version }}. Expected semver (e.g. 1.2.3)"
exit 1
fi
- name: Bump version
run: |
sed -i 's/VERSION = ".*"/VERSION = "${{ github.event.inputs.version }}"/' lib/n1_loader/version.rb
- name: Update changelog
env:
VERSION: ${{ github.event.inputs.version }}
run: |
DATE=$(date +%Y/%m/%d)
sed -i.bak -e "s|^## \[Unreleased\]|## [$VERSION] - $DATE|" CHANGELOG.md
rm -f CHANGELOG.md.bak
- name: Git status
run: git status
- name: Commit version bump
run: |
git config user.name "Evgenii Demin"
git config user.email "lawliet.djez@gmail.com"
git add lib/n1_loader/version.rb CHANGELOG.md
git commit -m "Release v${{ github.event.inputs.version }}"
git push origin HEAD
- name: Release gem
env:
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
run: bundle exec rake release
================================================
FILE: .github/workflows/rubocop.yml
================================================
name: Rubocop
on:
pull_request:
schedule:
- cron: '0 0 * * 0'
jobs:
rubocop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- name: Install dependencies
run: bundle install
- name: Run Rubocop
run: bundle exec rubocop
================================================
FILE: .github/workflows/tests.yml
================================================
name: RSpec tests
on:
pull_request:
schedule:
- cron: '0 0 * * 0'
jobs:
tests:
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
include:
- ruby-version: '2.7'
activerecord-gemfile: 'ar_5_latest'
ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'
- ruby-version: '2.7'
activerecord-gemfile: 'ar_6_latest'
ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'
- ruby-version: '3.0'
activerecord-gemfile: 'ar_6_latest'
ar_lazy_preload-gemfile: 'ar_lazy_preload_0.6.1'
- ruby-version: '3.0'
activerecord-gemfile: 'ar_7_latest'
ar_lazy_preload-gemfile: 'ar_lazy_preload_master'
- ruby-version: 'head'
activerecord-gemfile: 'ar_7_latest'
ar_lazy_preload-gemfile: 'ar_lazy_preload_master'
- ruby-version: 'head'
activerecord-gemfile: 'ar_8_latest'
ar_lazy_preload-gemfile: 'ar_lazy_preload_master'
env:
ACTIVERECORD_GEMFILE: ${{ matrix.activerecord-gemfile }}
AR_LAZY_PRELOAD_GEMFILE: ${{ matrix.ar_lazy_preload-gemfile }}
steps:
- uses: actions/checkout@v3
- name: Set up Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
- name: Install dependencies
run: bundle install
- name: Run Core tests
run: bundle exec rspec spec/n1_loader_spec.rb
- name: Run ActiveRecord tests
run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb
- name: Run ArLazyPreload tests
run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb
- name: Run Goldiloader tests
run: bundle exec rspec spec/n1_loader_spec.rb spec/activerecord_spec.rb spec/ar_lazy_preload_spec.rb
================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/.idea
.rspec_status
Gemfile.lock
.ruby-version
/vendor
================================================
FILE: .rspec
================================================
--format documentation
--color
--require spec_helper
================================================
FILE: .rubocop.yml
================================================
AllCops:
TargetRubyVersion: 2.5
Exclude:
- examples/**/*
- vendor/bundle/**/*
Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
Enabled: true
EnforcedStyle: double_quotes
Layout/LineLength:
Max: 120
Metrics/BlockLength:
Exclude:
- spec/**/*
================================================
FILE: CHANGELOG.md
================================================
## [3.0.0] - 2026/03/18
- Add support of [Goldiloader](https://github.com/KentaaNL/goldiloader)
## [2.2.1] - 2026/03/17
- Fix a rare thread-safety issue for setting context
- Raise errors if `n1_bind_to` received unexpected arguments
- Fix context propagation to loaded objects when using `n1_bind_to`
## [2.2.0] - 2026/03/16
- Support both identity and equality comparison in `Loader#for`. Identity lookup (via `object_id`) is tried first, with equality lookup as a fallback. Thanks [Alfonso Uceda](https://github.com/AlfonsoUceda) for reporting the issue!
- Clear binding when `n1_clear_cache` is called, so objects load independently after a cache reset.
## [2.1.0] - 2026/03/07
- Add `n1_bind_to` to support context sharing for plain Ruby objects without ActiveRecord, enabling N+1-free batch loading. Nested loading through N1Loader automatically propagates the shared context. Thanks [Pawel Pacana](https://github.com/paneq) for the feature request!
## [2.0.1] - 2025/12/28
- Add support of Rails 8.
## [2.0.0] - 2025/12/28
- Make loader thread-safe.
- Make loader idempotent.
## [1.7.4] - 2023/12/18
- Fix nested association does not preload properly. Thanks [dannyongtey](https://github.com/dannyongtey) for reporting and fixing the issue!
## [1.7.3] - 2023/08/04
- Decrease the package size by 60%.
## [1.7.2] - 2023/08/04
- Refactor core that ended up with speed boost.
## [1.7.1] - 2023/07/30
- Fix interface discrepancy for `N1LoaderReflection`. Thanks [Denis Talakevich](https://github.com/senid231) for suggesting it!
## [1.7.0] - 2023/07/30
Extend the flexibility of loading data comparison. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for suggesting it!
**BREAKING CHANGES:**
Loose comparison of loaded data. Before loaded data was initialized with identity comparator in mind:
```ruby
@loaded = {}.compare_by_identity
```
Now it will be:
```ruby
@loaded = {}
```
This might bring unwanted results for cases when strict comparison was wanted.
On the other hand, it gives more flexibility for many other cases, especially with isolated loader.
For example, this will work now, when it wasn't working before.
```ruby
# ActiveRecord object
object = Entity.first
# Initialize isolated loader
instance = loader.new([object])
# This was working before because the loaded object is identical to passed object by `#object_id`
instance.for(object)
# This wasn't working before because the loaded object is not identical to passed one by `#object_id`
#
# But it will be working now, because object == Entity.find(object.id)
instance.for(Entity.find(object.id))
```
If you need strict comparison support, please feel free to open the issue or the PR.
## [1.6.6] - 2023/07/30
- Fix naive check of required arguments. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for the issue!
## [1.6.5] - 2023/07/30
- Fix nested preloading for ActiveRecord 7. Thanks [Igor Gonchar](https://github.com/gigorok) for the issue!
## [1.6.4] - 2023/07/30
- Add support of `n1_optimized` ending with `?` (question mark). Thanks [Ilya Kamenko](https://github.com/Galathius) for the suggestion!
## [1.6.3] - 2022/12/30
- Performance optimization: avoid unnecessary calls. Thanks [Nazar Matus](https://github.com/FunkyloverOne) for the [contribution](https://github.com/djezzzl/n1_loader/pull/33).
## [1.6.2] - 2022/11/23
- Add fund metadata
## [1.6.1] - 2022/10/29
- Fix ArLazyPreload context setup when using isolated loaders for objects without the context.
## [1.6.0] - 2022/10/24
- Add support of ArLazyPreload context for isolated loaders.
## [1.5.1] - 2022/09/20
- 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)!
## [1.5.0] - 2022/05/01
- Add support of Rails 7
## [1.4.4] - 2022/04/29
- Inject `N1Loader::Loadable` to `ActiveRecord::Base` automatically
- Make `reload` to call `n1_clear_cache`
## [1.4.3] - 2022-04-13
- Add `default` support to arguments
## [1.4.2] - 2022-03-01
- Add n1_clear_cache method which is useful for cases like reload in ActiveRecord
## [1.4.1] - 2022-02-24
- Fix preloading of invalid objects
## [1.4.0] - 2022-02-22
- add support of optional arguments
BREAKING CHANGES:
- rework arguments to use single definition through `argument <name>` only
- use keyword arguments
## [1.3.0] - 2022-02-22
- add support of named arguments with `argument <name>`
BREAKING CHANGES:
- rename `n1_load` to `n1_optimized`
- rework `def self.arguments_key` to `cache_key`
## [1.2.0] - 2022-01-14
- Introduce arguments support.
## [1.1.0] - 2021-12-27
- Introduce `fulfill` method to abstract the storage.
## [1.0.0] - 2021-12-26
- Various of great features.
## [0.1.0] - 2021-12-16
- Initial release.
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at TODO: Write your email address. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
================================================
FILE: Gemfile
================================================
# frozen_string_literal: true
source "https://rubygems.org"
# Specify your gem's dependencies in n1_loader.gemspec
gemspec
# Hack to make Github work with Circle CI job names with slashes
gemfiles = []
gemfiles << "activerecord-gemfiles/#{ENV["ACTIVERECORD_GEMFILE"]}.gemfile" if ENV["ACTIVERECORD_GEMFILE"]
gemfiles << "ar_lazy_preload-gemfiles/#{ENV["AR_LAZY_PRELOAD_GEMFILE"]}.gemfile" if ENV["AR_LAZY_PRELOAD_GEMFILE"]
gemfiles << "goldiloader-gemfiles/#{ENV["GOLDILOADER_GEMFILE"]}.gemfile" if ENV["GOLDILOADER_GEMFILE"]
gemfiles.each do |path|
eval(File.read(path)) # rubocop:disable Security/Eval
end
================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)
Copyright (c) 2021 TODO: Write your name
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# N1Loader
[![Gem Version][3]][4]
[![][11]][12]
[![][13]][14]
[![][9]][10]
N1Loader is designed to provide a simple way for avoiding [N+1 issues][7] 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][15] or [donate][10].
> Your support is an incredible motivation and the biggest reward for my hard work.
___Support:___ ActiveRecord 5, 6, 7, and 8.
Follow me and stay tuned for the updates:
- [LinkedIn](https://www.linkedin.com/in/evgeniydemin/)
- [Medium](https://evgeniydemin.medium.com/)
- [Twitter](https://twitter.com/EvgeniyDemin/)
- [GitHub](https://github.com/djezzzl)
## Killer feature for GraphQL API
N1Loader in combination with [ArLazyPreload][6] or [Goldiloader][16] is a killer feature for your GraphQL API.
Give it a try now and see incredible results instantly! Check out the [example](examples/graphql.rb) and start benefiting from it in your projects!
```ruby
gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
# or
gem 'n1_loader', require: 'n1_loader/goldiloader'
```
## Enhance [ActiveRecord][5]
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](guides/enhanced-activerecord.md) with examples or its [short version](examples/active_record_integration.rb).
```ruby
gem 'n1_loader', require: 'n1_loader/active_record'
```
Are you ready to forget about N+1 once and for all? Install [ArLazyPreload][6] or [Goldiloader][16] and see dreams come true!
```ruby
gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
# or
gem 'n1_loader', require: 'n1_loader/goldiloader'
```
## Standalone mode
Are you not working with [ActiveRecord][5]? N1Loader is ready to be used as standalone solution! ([full snippet](examples/core.rb))
```ruby
gem 'n1_loader'
```
Want lazy, N+1-free loading without explicit preloading? Use `n1_bind_to` to share context across a collection of plain Ruby objects. ([full snippet](examples/n1_bind_to.rb))
```ruby
users = [User.new, User.new, User.new]
# Bind users to the collection — lazy access is now automatically batched
users.each { |user| user.n1_bind_to(users) }
users.map(&:optimized_call) # loads all in a single batch, no N+1
```
## 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](examples/isolated_loader.rb).
Let's look at simple example below ([full snippet](examples/active_record_integration.rb)):
```ruby
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](examples/arguments_support.rb))
```ruby
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](examples/isolated_loader.rb)
- Loads data [lazily](examples/lazy_loading.rb)
- Loaders can be [shared](examples/shared_loader.rb) between multiple classes
- Loaded data can be [re-fetched](examples/reloading.rb)
- Loader can be optimized for [single cases](examples/single_case.rb)
- Loader support [arguments](examples/arguments_support.rb)
- Has [integration](examples/active_record_integration.rb) with [ActiveRecord][5] which makes it brilliant
- Has [integration](examples/ar_lazy_integration.rb) with [ArLazyPreload][6] which makes it excellent
- Has [integration](examples/goldiloader_integration.rb) with [Goldiloader][16] which makes it excellent
- Supports [context sharing](examples/n1_bind_to.rb) for plain Ruby objects without ActiveRecord
### Feature killer for [ArLazyPreload][6] and [Goldiloader][16] integration with isolated loaders
In [version 1.6.0](CHANGELOG.md#160---20221019) isolated loaders were integrated with [ArLazyPreload][6] context.
This means, it isn't required to inject `N1Loader` into your [ActiveRecord][5] 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](examples/ar_lazy_integration_with_isolated_loader.rb) for ArLazyPreload or the [example](examples/goldiloader_integration_with_isolated_loader.rb) for Goldiloader.
_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.
### Context sharing for plain Ruby objects with `n1_bind_to`
In [version 2.1.0](CHANGELOG.md#210---20260307) context sharing was added for plain Ruby objects.
This allows you to get lazy, N+1-free loading without [ActiveRecord][5] or explicit preloading.
By calling `n1_bind_to(collection)` on each element, you bind them to their shared collection.
When any loader is triggered on a bound element, it automatically batch-loads for the entire collection — and nested loaders propagate the context automatically as well.
Have a look at the [example](examples/n1_bind_to.rb) to see how simple it is.
## 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](https://opencollective.com/n1_loader#backer)]
<a href="https://opencollective.com/n1_loader/backer/0/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/0/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/1/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/1/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/2/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/2/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/3/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/3/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/4/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/4/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/5/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/5/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/6/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/6/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/7/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/7/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/8/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/8/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/9/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/9/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/10/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/10/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/11/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/11/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/12/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/12/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/13/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/13/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/14/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/14/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/15/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/15/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/16/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/16/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/17/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/17/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/18/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/18/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/19/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/19/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/20/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/20/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/21/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/21/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/22/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/22/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/23/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/23/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/24/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/24/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/25/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/25/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/26/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/26/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/27/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/27/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/28/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/backer/29/website" target="_blank"><img src="https://opencollective.com/n1_loader/backer/29/avatar.svg"></a>
### 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](https://opencollective.com/n1_loader#sponsor)]
<a href="https://opencollective.com/n1_loader/sponsor/0/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/1/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/2/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/3/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/4/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/5/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/6/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/7/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/8/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/9/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/10/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/11/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/12/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/13/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/14/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/15/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/16/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/17/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/18/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/19/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/20/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/21/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/22/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/23/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/24/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/25/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/26/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/27/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/28/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/n1_loader/sponsor/29/website" target="_blank"><img src="https://opencollective.com/n1_loader/sponsor/29/avatar.svg"></a>
## 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](CODE_OF_CONDUCT.md).
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## 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](CODE_OF_CONDUCT.md).
## Changelog
*N1Loader*'s changelog is available [here](CHANGELOG.md).
## Copyright
Copyright (c) Evgeniy Demin. See [LICENSE.txt](LICENSE.txt) for further details.
[3]: https://badge.fury.io/rb/n1_loader.svg
[4]: https://badge.fury.io/rb/n1_loader
[5]: https://github.com/rails/rails/tree/main/activerecord
[6]: https://github.com/DmitryTsepelev/ar_lazy_preload
[7]: https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping
[8]: https://github.com/djezzzl/n1_loader
[9]: https://opencollective.com/n1_loader/tiers/badge.svg
[10]: https://opencollective.com/n1_loader#support
[11]: https://github.com/djezzzl/n1_loader/actions/workflows/tests.yml/badge.svg?branch=master
[12]: https://github.com/djezzzl/n1_loader/actions/workflows/tests.yml?query=event%3Aschedule
[13]: https://github.com/djezzzl/n1_loader/actions/workflows/rubocop.yml/badge.svg?branch=master
[14]: https://github.com/djezzzl/n1_loader/actions/workflows/rubocop.yml?query=event%3Aschedule
[15]: https://github.com/djezzzl/n1_loader#contributing
[16]: https://github.com/KentaaNL/goldiloader
================================================
FILE: Rakefile
================================================
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)
require "rubocop/rake_task"
RuboCop::RakeTask.new
task default: %i[spec rubocop]
================================================
FILE: activerecord-gemfiles/ar_5_latest.gemfile
================================================
# frozen_string_literal: true
gem "activerecord", "~> 5"
gem "concurrent-ruby", "= 1.3.4"
================================================
FILE: activerecord-gemfiles/ar_6_latest.gemfile
================================================
# frozen_string_literal: true
gem "activerecord", "~> 6"
gem "benchmark"
gem "bigdecimal"
gem "concurrent-ruby", "= 1.3.4"
gem "sqlite3", "~> 1.4"
================================================
FILE: activerecord-gemfiles/ar_7_latest.gemfile
================================================
# frozen_string_literal: true
gem "activerecord", "~> 7"
gem "benchmark"
gem "bigdecimal"
gem "concurrent-ruby", "= 1.3.4"
gem "sqlite3", "< 2.4.0"
================================================
FILE: activerecord-gemfiles/ar_8_latest.gemfile
================================================
# frozen_string_literal: true
gem "activerecord", "~> 8"
gem "benchmark"
gem "bigdecimal"
gem "sqlite3", "< 2.4.0"
================================================
FILE: ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile
================================================
# frozen_string_literal: true
gem "ar_lazy_preload", "= 0.6.1"
================================================
FILE: ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile
================================================
# frozen_string_literal: true
gem "ar_lazy_preload", git: "https://github.com/DmitryTsepelev/ar_lazy_preload", branch: "master"
================================================
FILE: bin/console
================================================
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "n1_loader"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start(__FILE__)
================================================
FILE: bin/setup
================================================
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here
================================================
FILE: examples/active_record_integration.rb
================================================
# frozen_string_literal: true
require "n1_loader/active_record"
require_relative 'context/setup_database'
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
fill_database
# Has N+1
p User.all.map { |user| user.payments.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.sum(&:amount) }
# Has no N+1 and calculation is the most efficient
p User.all.includes(:payments_total).map(&:payments_total)
================================================
FILE: examples/ar_lazy_integration.rb
================================================
# frozen_string_literal: true
require "n1_loader/ar_lazy_preload"
require_relative 'context/setup_ar_lazy'
require_relative 'context/setup_database'
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
fill_database
# Has N+1
p User.all.map { |user| user.payments.sum(&:amount) }
# Has no N+1 and loads only required data
p User.preload_associations_lazily.map(&:payments_total)
# or
ArLazyPreload.config.auto_preload = true
User.all.map(&:payments_total)
================================================
FILE: examples/ar_lazy_integration_with_isolated_loader.rb
================================================
# frozen_string_literal: true
require "n1_loader/ar_lazy_preload"
require_relative 'context/setup_ar_lazy'
require_relative 'context/setup_database'
class Loader < N1Loader::Loader
def perform(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 User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
validates :amount, presence: true
end
fill_database
# Has N+1 and loads redundant data
p User.all.map { |user| user.payments.sum(&:amount) }
# Has no N+1 and loads only required data
p User.preload_associations_lazily.all.map { |user| Loader.for(user) }
# or
ArLazyPreload.config.auto_preload = true
p User.all.map { |user| Loader.for(user) }
================================================
FILE: examples/arguments_support.rb
================================================
# frozen_string_literal: true
require "n1_loader/active_record"
require_relative 'context/setup_database'
class User < ActiveRecord::Base
has_many :payments
n1_optimized :payments_total do
# Arguments can be:
# argument :something, optional: true
# argument :something, default: -> { 100 }
#
# Note: do not use mutable (mostly timing related) defaults like:
# argument :from, default -> { 2.minutes.from_now }
# because such values will be unique for every loader call which will make N+1 issue stay
argument :from
argument :to
# This is used to define logic how loaders are compared to each other
# default is:
# cache_key { *arguments.map(&:object_id) }
cache_key { [from, 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
fill_database
from = 2.days.ago
to = 1.day.ago
# Has N+1
p User.all.map { |user|
user.payments.select do |payment|
payment.created_at >= from && payment.created_at <= to
end.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 do |payment|
payment.created_at >= from && payment.created_at <= to
end.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) }
================================================
FILE: examples/context/service.rb
================================================
# 3rd party service, or database, or anything else that can perform in batches
class Service
def self.count
@count ||= 0
end
def self.increase!
@count = (@count || 0) + 1
end
def self.receive(*users)
increase!
users.flatten.map(&:object_id)
end
def self.single(user)
user.object_id
end
end
================================================
FILE: examples/context/setup_ar_lazy.rb
================================================
ActiveSupport.on_load(:active_record) do
ActiveRecord::Base.include(ArLazyPreload::Base)
ActiveRecord::Relation.prepend(ArLazyPreload::Relation)
ActiveRecord::AssociationRelation.prepend(ArLazyPreload::AssociationRelation)
ActiveRecord::Relation::Merger.prepend(ArLazyPreload::Merger)
[
ActiveRecord::Associations::CollectionAssociation,
ActiveRecord::Associations::Association
].each { |klass| klass.prepend(ArLazyPreload::Association) }
ActiveRecord::Associations::CollectionAssociation.prepend(ArLazyPreload::CollectionAssociation)
ActiveRecord::Associations::CollectionProxy.prepend(ArLazyPreload::CollectionProxy)
end
================================================
FILE: examples/context/setup_database.rb
================================================
require "sqlite3"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.drop_table(table, force: :cascade)
end
ActiveRecord::Schema.verbose = false
ActiveRecord::Base.logger = Logger.new($stdout)
ActiveRecord::Schema.define(version: 1) do
create_table(:payments) do |t|
t.belongs_to :user
t.integer :amount
t.timestamps
end
create_table(:users)
end
def fill_database
10.times do
user = User.create!
10.times do
Payment.create!(user: user, amount: rand(1000))
end
end
end
================================================
FILE: examples/core.rb
================================================
# frozen_string_literal: true
require "n1_loader"
require_relative 'context/service'
# Class that wants to request 3rd party service without N+1
class User
include N1Loader::Loadable
def unoptimized_call
Service.receive(self)[0]
end
n1_optimized :optimized_call do |users|
data = Service.receive(users)
users.each_with_index do |user, index|
fulfill(user, data[index])
end
end
end
# works fine for single case
user = User.new
p "Works correctly: #{user.unoptimized_call == user.optimized_call}"
users = [User.new, User.new]
# Has N+1
count_before = Service.count
p users.map(&:unoptimized_call)
p "Has N+1 #{Service.count == count_before + users.count}"
# Has no N+1 via explicit preloading
count_before = Service.count
N1Loader::Preloader.new(users).preload(:optimized_call)
p users.map(&:optimized_call)
p "Has no N+1: #{Service.count == count_before + 1}"
users = [User.new, User.new]
# Has no N+1 via n1_bind_to context sharing (see examples/n1_bind_to.rb)
users.each { |user| user.n1_bind_to(users) }
count_before = Service.count
p users.map(&:optimized_call)
p "Has no N+1: #{Service.count == count_before + 1}"
================================================
FILE: examples/goldiloader_integration.rb
================================================
# frozen_string_literal: true
require "n1_loader/goldiloader"
require_relative "context/setup_database"
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
fill_database
# Has N+1
p(User.all.map { |user| user.payments.sum(&:amount) })
# Has no N+1 and loads only required data (Goldiloader auto-batches by default)
p User.all.map(&:payments_total)
================================================
FILE: examples/goldiloader_integration_with_isolated_loader.rb
================================================
# frozen_string_literal: true
require "n1_loader/goldiloader"
require_relative "context/setup_database"
class Loader < N1Loader::Loader
def perform(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 User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
validates :amount, presence: true
end
fill_database
# Has N+1 and loads redundant data
p(User.all.map { |user| user.payments.sum(&:amount) })
# Has no N+1 and loads only required data (Goldiloader auto-batches by default)
p(User.all.map { |user| Loader.for(user) })
================================================
FILE: examples/graphql.rb
================================================
# frozen_string_literal: true
require "n1_loader/ar_lazy_preload"
require 'graphql'
require_relative 'context/setup_database'
require_relative 'context/setup_ar_lazy'
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
10.times do
user = User.create!
10.times do
Payment.create!(user: user, amount: rand(1000))
end
end
ArLazyPreload.config.auto_preload = true
# Or use +preload_associations_lazily+ when loading objects from database
class UserType < GraphQL::Schema::Object
field :payments_total, Integer
end
class QueryType < GraphQL::Schema::Object
field :users, [UserType]
def users
User.all
end
end
class Schema < GraphQL::Schema
query QueryType
end
query_string = <<~GQL
{
users {
paymentsTotal
}
}
GQL
# No N+1. And never will be!
p Schema.execute(query_string)['data']
================================================
FILE: examples/isolated_loader.rb
================================================
require 'n1_loader'
class IsolatedLoader < N1Loader::Loader
def perform(elements)
elements.each { |element| fulfill(element, [element]) }
end
end
objects = [1, 2, 3, 4]
loader = IsolatedLoader.new(objects)
objects.each do |object|
loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
end
================================================
FILE: examples/lazy_loading.rb
================================================
require 'n1_loader'
require_relative 'context/service'
# Class that wants to request 3rd party service without N+1
class User
include N1Loader::Loadable
n1_optimized :optimized_call do |users|
data = Service.receive(users)
users.each_with_index do |user, index|
fulfill(user, data[index])
end
end
end
users = [User.new, User.new, User.new]
# Initialized loader but didn't perform it yet
N1Loader::Preloader.new(users).preload(:optimized_call)
p "No calls yet: #{Service.count == 0}"
# First time loading
users.map(&:optimized_call)
p "First time loaded: #{Service.count == 1}"
================================================
FILE: examples/n1_bind_to.rb
================================================
# frozen_string_literal: true
require "n1_loader"
require_relative 'context/service'
# Class that wants to request 3rd party service without N+1
class User
include N1Loader::Loadable
n1_optimized :optimized_call do |users|
data = Service.receive(users)
users.each_with_index do |user, index|
fulfill(user, data[index])
end
end
end
users = [User.new, User.new, User.new]
# Without n1_bind_to: each user lazily loads independently causing N+1
count_before = Service.count
p users.map(&:optimized_call)
p "Has N+1: #{Service.count == count_before + users.count}"
users = [User.new, User.new, User.new]
# With n1_bind_to: bind users to the collection so lazy loading is automatically batched
users.each { |user| user.n1_bind_to(users) }
count_before = Service.count
p users.map(&:optimized_call)
p "Has no N+1: #{Service.count == count_before + 1}"
================================================
FILE: examples/reloading.rb
================================================
require 'n1_loader'
require_relative 'context/service'
class User
include N1Loader::Loadable
n1_optimized :optimized_call do |users|
data = Service.receive(users)
users.each_with_index do |user, index|
fulfill(user, data[index])
end
end
end
users = [User.new, User.new, User.new]
# Initialized loader but didn't perform it yet
N1Loader::Preloader.new(users).preload(:optimized_call)
p "No calls yet: #{Service.count == 0}"
# First time loading
users.map(&:optimized_call)
p "First time loaded: #{Service.count == 1}"
users.first.optimized_call(reload: true)
p "Reloaded for this object only: #{Service.count == 2}"
users.first.n1_clear_cache
users.first.optimized_call
p "Reloaded for this object only: #{Service.count == 3}"
================================================
FILE: examples/shared_loader.rb
================================================
require 'n1_loader'
require_relative 'context/service'
# Loader that will be shared between multiple classes
class SharedLoader < N1Loader::Loader
def perform(objects)
data = Service.receive(objects)
objects.each_with_index do |user, index|
fulfill(user, data[index])
end
end
end
class User
include N1Loader::Loadable
n1_optimized :optimized_call, SharedLoader
end
class Payment
include N1Loader::Loadable
n1_optimized :optimized_call, SharedLoader
end
objects = [User.new, Payment.new, User.new, Payment.new]
N1Loader::Preloader.new(objects).preload(:optimized_call)
# First time loading for all objects
objects.map(&:optimized_call)
p "Loaded for all once: #{Service.count == 1}"
================================================
FILE: examples/single_case.rb
================================================
require 'n1_loader'
require_relative 'context/service'
# Loader that will be shared between multiple classes
class OptimizedLoader < N1Loader::Loader
def perform(objects)
data = Service.receive(objects)
objects.each_with_index do |user, index|
fulfill(user, data[index])
end
end
def single(object)
Service.single(object)
end
end
class User
include N1Loader::Loadable
n1_optimized :optimized_call, OptimizedLoader
end
objects = [User.new, User.new]
N1Loader::Preloader.new(objects).preload(:optimized_call)
objects.map(&:optimized_call)
p "Used multi-case perform: #{Service.count == 1}"
User.new.optimized_call
p "Used single-case perform: #{Service.count == 1}"
================================================
FILE: goldiloader-gemfiles/goldiloader.gemfile
================================================
# frozen_string_literal: true
gem "goldiloader"
================================================
FILE: guides/enhanced-activerecord.md
================================================
# Enhanced ActiveRecord
- Do you like `ActiveRecord` preloading?
- How many times have you resolved your N+1 issues with `includes` or `preload`?
- Do you know that preloading has limitations?
In this guide, I'd like to share with you tips and tricks about ActiveRecord
preloading and how you can enhance it to the next level.
Let's start by describing the models.
```ruby
# The model represents users in our application.
class User < ActiveRecord::Base
# Every user may have from 0 to many payments.
has_many :payments
end
# The model represents payments in our application.
class Payment < ActiveRecord::Base
# Every payment belongs to a user.
belongs_to :user
end
```
Assuming we want to iterate over a group of users and check how many payments they have, we may do:
```ruby
# The query we want to use to fetch users from the database.
users = User.all
# Iteration over selected users.
users.each do |user|
# Print amount of user's payments.
# This query will be called for every user, bringing an N+1 issue.
p user.payments.count
end
```
We can fix the N+1 issue above in a second.
We need to add ActiveRecord's `includes` to the query that fetches users.
```ruby
# The query to fetch users with preload payments for every selected user.
users = User.includes(:payments).all
```
Then, we can iterate over the group again without the N+1 issue.
```ruby
users.each do |user|
p user.payments.count
end
```
Experienced with ActiveRecord person may notice that the iteration above still will have an N+1 issue.
The reason is the `.count` method and its behavior.
This issue brings us to the first tip.
### Tip 1. `count` vs `size` vs `length`
- `count` - always queries the database with `COUNT` query;
- `size` - queries the database with `COUNT` only when there is no preloaded data, returns array length otherwise;
- `length` - always returns array length, in case there is no data, load it first.
_Note:_ be careful with `size` as ordering is critical.
Meaning, for `user = User.first`
```ruby
# Does `COUNT` query
user.payments.size
# Does `SELECT` query
user.payments.each { |payment| }
```
is different from
```ruby
# Does `SELECT` query
user.payments.each { |payment| }
# No query
user.payments.size
```
You may notice that the above solution loads all payment information when the amount is only needed.
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).
To use that, you need to add `payments_count` field to `users` table and adjust `Payment` model.
```ruby
# Migration to add `payments_count` to `users` table.
class AddPaymentsCountToUsers < ActiveRecord::Migration
def change
add_column :users, :payments_count, :integer, default: 0, null: false
end
end
# Change `belongs_to` to have `counter_cache` option.
class Payment < ActiveRecord::Base
belongs_to :user, counter_cache: true
end
```
_Note:_ avoid adding or removing payments from the database directly or through `insert_all`/`delete`/`delete_all` as
`counter_cache` is using ActiveRecord callbacks to update the field's value.
It's worth mentioning [counter_culture](https://github.com/magnusvk/counter_culture) alternative that has many features compared with the built-in `counter_cache`
## Associations with arguments
Now, let's assume we want to fetch the number of payments in a time frame for every user in a group.
```ruby
from = 1.months.ago
to = Time.current
# Query to fetch users.
users = User.all
users.each do |user|
# Print the number of payments in a time frame for every user.
# Database query will be triggered for every user, meaning it has an N+1 issue.
p user.payments.where(created_at: from...to).count
end
```
ActiveRecord supports defining associations with arguments.
```ruby
class User < ActiveRecord::Base
has_many :payments, -> (from, to) { where(created_at: from...to) }
end
```
Unfortunately, such associations are not possible to preload with `includes`.
Gladly, there is a solution with [N1Loader](https://github.com/djezzzl/n1_loader/).
```ruby
# Install gem dependencies.
require 'n1_loader/active_record'
class User < ActiveRecord::Base
n1_optimized :payments_count do
argument :from
argument :to
def perform(users)
# Fetch the payment number once for all users.
payments = Payment.where(user: users).where(created_at: from...to).group(:user_id).count
users.each do |user|
# Assign preloaded data to every user.
# Note: it doesn't use any promises.
fulfill(user, payments[user.id])
end
end
end
end
from = 1.month.ago
to = Time.current
# Preload `payments` N1Loader "association". Doesn't query the database yet.
users = User.includes(:payments_count).all
users.each do |user|
# Queries the database once, meaning has no N+1 issues.
p user.payments_count(from, to)
end
```
Let's look at another example. Assuming we want to fetch the last payment for every user.
We can try to define scoped `has_one` association and use that.
```ruby
class User < ActiveRecord::Base
has_one :last_payment, -> { order(id: :desc) }, class_name: 'Payment'
end
```
We can see that preloading is working.
```ruby
users = User.includes(:last_payment)
users.each do |user|
# No N+1. Last payment was returned.
p user.last_payment
end
```
At first glance, we may think everything is alright. Unfortunately, it is not.
### Tip 2. Enforce `has_one` associations on the database level
ActiveRecord, fetches all available payments for every user with provided order and then assigns only first payment to the association.
First, such querying is inefficient as we load many redundant information.
But most importantly, this association may lead to big issues. Other engineers may use it, for example,
for `joins(:last_payment)`. Assuming that association has strict agreement on the database level that
a user may have none or a single payment in the database. Apparently, it may not be the case, and some queries
will return unexpected data.
Described issues may be found with [DatabaseConsistency](https://github.com/djezzzl/database_consistency).
Back to the task, we can solve it with [N1Loader](https://github.com/djezzzl/n1_loader) in the following way
```ruby
require 'n1_loader/active_record'
class User < ActiveRecord::Base
n1_optimized :last_payment do |users|
subquery = Payment.select('MAX(id)').where(user: users)
payments = Payment.where(id: subquery).index_by(&:user_id)
users.each do |user|
fulfill(user, payments[user.id])
end
end
end
users = User.includes(:last_payment).all
users.each do |user|
# Queries the database once, meaning no N+1.
p user.last_payment
end
```
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.
Gladly, there is a simple solution! [ArLazyPreload](https://github.com/DmitryTsepelev/ar_lazy_preload) will make N+1 disappear just by enabling it.
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.
And it works with ActiveRecord and N1Loader perfectly!
Let's look at the example.
```ruby
# Require N1Loader with ArLazyPreload integration
require 'n1_loader/ar_lazy_preload'
# Enable ArLazyPreload globally, so you don't need to care about `includes` anymore
ArLazyPreload.config.auto_preload = true
class User < ActiveRecord::Base
has_many :payments
n1_optimized :last_payment do |users|
subquery = Payment.select('MAX(id)').where(user: users)
payments = Payment.where(id: subquery).index_by(&:user_id)
users.each do |user|
fulfill(user, payments[user.id])
end
end
end
# no need to specify `includes`
users = User.all
users.each do |user|
p user.payments # no N+1
p user.last_payment # no N+1
end
```
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.
It works great with GraphQL API too. Give it and try and share your feedback!
================================================
FILE: lib/n1_loader/active_record/associations_preloader.rb
================================================
# frozen_string_literal: true
module N1Loader
module ActiveRecord
module Associations
module Preloader # :nodoc:
N1LoaderReflection = Struct.new(:name, :loader) do
def options
{}
end
def deprecated?
false
end
def through_reflection?
false
end
end
def preloaders_for_reflection(reflection, records)
return super unless reflection.is_a?(N1LoaderReflection)
N1Loader::Preloader.new(records).preload(reflection.name)
end
def grouped_records # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
n1_load_records, records = source_records.partition do |record|
record.class.respond_to?(:n1_loaders) && record.class.n1_loaders[association]
end
h = n1_load_records.group_by do |record|
N1LoaderReflection.new(association, record.class.n1_loaders[association])
end
polymorphic_parent = !root? && parent.polymorphic?
records.each do |record|
reflection = record.class._reflect_on_association(association)
next if polymorphic_parent && !reflection || !record.association(association).klass
(h[reflection] ||= []) << record
end
h
end
end
end
end
end
================================================
FILE: lib/n1_loader/active_record/associations_preloader_v5.rb
================================================
# frozen_string_literal: true
module N1Loader
module ActiveRecord
module Associations
module Preloader # :nodoc:
N1LoaderReflection = Struct.new(:name, :loader) do
def options
{}
end
end
def preloaders_for_one(association, records, scope)
grouped_records(association, records).flat_map do |reflection, klasses|
next N1Loader::Preloader.new(records).preload(reflection.name) if reflection.is_a?(N1LoaderReflection)
klasses.map do |rhs_klass, rs|
loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
loader.run self
loader
end
end
end
def grouped_records(association, records)
n1_load_records, records = records.partition do |record|
record.class.respond_to?(:n1_loaders) && record.class.n1_loaders.key?(association)
end
hash = n1_load_records.group_by do |record|
N1LoaderReflection.new(association, record.class.n1_loaders[association])
end
hash.merge(super)
end
end
end
end
end
================================================
FILE: lib/n1_loader/active_record/associations_preloader_v6.rb
================================================
# frozen_string_literal: true
module N1Loader
module ActiveRecord
module Associations
module Preloader # :nodoc:
N1LoaderReflection = Struct.new(:name, :loader) do
def options
{}
end
end
def preloaders_for_reflection(reflection, records, scope)
return super unless reflection.is_a?(N1LoaderReflection)
N1Loader::Preloader.new(records).preload(reflection.name)
end
def grouped_records(association, records, polymorphic_parent)
n1_load_records, records = records.partition do |record|
record.class.respond_to?(:n1_loaders) && record.class.n1_loaders[association]
end
hash = n1_load_records.group_by do |record|
N1LoaderReflection.new(association, record.class.n1_loaders[association])
end
hash.merge(super)
end
end
end
end
end
================================================
FILE: lib/n1_loader/active_record/base.rb
================================================
# frozen_string_literal: true
module N1Loader
module ActiveRecord
# Extension module for ActiveRecord::Base
module Base
extend ActiveSupport::Concern
include N1Loader::Loadable
# Clear N1Loader cache on reloading the object
def reload(*)
n1_clear_cache
super
end
end
end
end
================================================
FILE: lib/n1_loader/active_record/loader.rb
================================================
# frozen_string_literal: true
N1Loader::Loader.define_method :preloaded_records do
@preloaded_records ||= loaded? && loaded_by_value.values.flatten
end
================================================
FILE: lib/n1_loader/active_record/loader_collection.rb
================================================
# frozen_string_literal: true
N1Loader::LoaderCollection.define_method :preloaded_records do
raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments" if loader_class.arguments
with.preloaded_records
end
N1Loader::LoaderCollection.define_method :runnable_loaders do
[self]
end
N1Loader::LoaderCollection.define_method :run? do
true
end
N1Loader::LoaderCollection.define_method :future_classes do
[]
end
================================================
FILE: lib/n1_loader/active_record.rb
================================================
# frozen_string_literal: true
# Load core library
require_relative "../n1_loader"
# Load integration dependency
require "active_record"
module N1Loader
module ActiveRecord
class InvalidPreloading < N1Loader::Error; end
end
end
# Library integration
ActiveSupport.on_load(:active_record) do
require_relative "active_record/loader"
require_relative "active_record/loader_collection"
require_relative "active_record/base"
case ActiveRecord::VERSION::MAJOR
when 6
require_relative "active_record/associations_preloader_v6"
ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
when 5
require_relative "active_record/associations_preloader_v5"
ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
else
require_relative "active_record/associations_preloader"
ActiveRecord::Associations::Preloader::Branch.prepend(N1Loader::ActiveRecord::Associations::Preloader)
end
ActiveRecord::Base.include(N1Loader::ActiveRecord::Base)
end
================================================
FILE: lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
================================================
# frozen_string_literal: true
module N1Loader
module ArLazyPreload
# Context builder for N1Loader
class AssociatedContextBuilder < ::ArLazyPreload::AssociatedContextBuilder
attr_reader :records
def initialize(parent_context:, association_name:, records:)
super(parent_context: parent_context, association_name: association_name)
@records = records
end
def perform
::ArLazyPreload::Context.register(
records: records.flatten(1).select { |record| record.respond_to?(:lazy_preload_context=) },
association_tree: child_association_tree,
auto_preload: parent_context.auto_preload?
)
end
end
end
end
================================================
FILE: lib/n1_loader/ar_lazy_preload/context.rb
================================================
# frozen_string_literal: true
# Returns cached N1Loader::LoaderCollection from context for a loader.
# In case there is none yet, saves passed block to a cache.
ArLazyPreload::Contexts::BaseContext.define_method :fetch_n1_loader_collection do |loader, &block|
(@n1_loader_collections ||= {})[loader] ||= block.call
end
================================================
FILE: lib/n1_loader/ar_lazy_preload/context_adapter.rb
================================================
# frozen_string_literal: true
module N1Loader
module ArLazyPreload
# Context adapter for injected N1Loader loaders.
class ContextAdapter
attr_reader :context
delegate_missing_to :context
def initialize(context)
@context = context
end
# Assign initialized preloader to +association_name+ in case it wasn't yet preloaded within the given context.
def try_preload_lazily(association_name)
return unless context&.send(:association_needs_preload?, association_name)
perform_preloading(association_name)
end
# Initialize preloader for +association_name+ with context builder callback.
# The callback will be executed when on records load.
def perform_preloading(association_name)
context_setup = lambda { |records|
AssociatedContextBuilder.prepare(
parent_context: self,
association_name: association_name,
records: records
)
}
N1Loader::Preloader.new(records, context_setup).preload(association_name)
end
end
end
end
================================================
FILE: lib/n1_loader/ar_lazy_preload/loadable.rb
================================================
# frozen_string_literal: true
module N1Loader
module ArLazyPreload
module Loadable # :nodoc:
def n1_loader(name)
return n1_loaders[name] if n1_loaders[name]
ContextAdapter.new(lazy_preload_context).try_preload_lazily(name) if respond_to?(:lazy_preload_context)
super
end
end
end
end
================================================
FILE: lib/n1_loader/ar_lazy_preload/loader.rb
================================================
# frozen_string_literal: true
# Raised when a single object without ArLazyPreload context support was passed to an isolated loader.
N1Loader::Loader::UnsupportedArLazyPreload = Class.new(StandardError)
# Defines a singleton method method that allows isolated loaders
# to use ArLazyPreload context without passing sibling records.
N1Loader::Loader.define_singleton_method(:for) do |element, **args|
# It is required to have an ArLazyPreload context supported
raise N1Loader::Loader::UnsupportedArLazyPreload unless element.respond_to?(:lazy_preload_context)
if element.lazy_preload_context.nil?
ArLazyPreload::Context.register(
records: [element],
association_tree: [],
auto_preload: true
)
end
# Fetch or initialize loader from ArLazyPreload context
loader_collection = element.lazy_preload_context.fetch_n1_loader_collection(self) do
context_setup = lambda { |records|
N1Loader::ArLazyPreload::AssociatedContextBuilder.prepare(
parent_context: element.lazy_preload_context,
association_name: "cached_n1_loader_collection_#{self}".downcase.to_sym,
records: records
)
}
N1Loader::LoaderCollection.new(self, element.lazy_preload_context.records).tap do |collection|
collection.context_setup = context_setup
end
end
# Fetch value from loader
loader_collection.with(**args).for(element)
end
================================================
FILE: lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb
================================================
# frozen_string_literal: true
module N1Loader
module ArLazyPreload
# A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.
module LoaderCollectionPatch
attr_accessor :context_setup
def with(**args)
result = super
result.context_setup = context_setup if context_setup && result.context_setup.nil?
result
end
end
end
end
================================================
FILE: lib/n1_loader/ar_lazy_preload/loader_patch.rb
================================================
# frozen_string_literal: true
module N1Loader
module ArLazyPreload
# A patch to {N1Loader::Loader} to setup lazy context lazily.
module LoaderPatch
attr_accessor :context_setup
def loaded?
return true if @already_loaded && @already_context
super
synchronize { non_thread_safe_context_setting unless @already_context }
true
end
def non_thread_safe_context_setting
return if @already_context
context_setup&.call(loaded_by_identity.values.flatten)
@already_context = true
end
end
end
end
================================================
FILE: lib/n1_loader/ar_lazy_preload/preloader_patch.rb
================================================
# frozen_string_literal: true
module N1Loader
module ArLazyPreload
# A patch to {N1Loader::Preloader} setup lazy context lazily.
module PreloaderPatch
def initialize(elements, context_setup = nil)
super(elements)
@context_setup = context_setup
end
def preload(*keys)
super.each do |loader_collection|
loader_collection.context_setup = context_setup
end
end
private
attr_reader :context_setup
end
end
end
================================================
FILE: lib/n1_loader/ar_lazy_preload.rb
================================================
# frozen_string_literal: true
# Load core library
require_relative "active_record"
# Load integration dependency
require "rails"
require "ar_lazy_preload"
# Library integration
require_relative "ar_lazy_preload/loadable"
require_relative "ar_lazy_preload/context_adapter"
require_relative "ar_lazy_preload/associated_context_builder"
require_relative "ar_lazy_preload/loader_collection_patch"
require_relative "ar_lazy_preload/preloader_patch"
require_relative "ar_lazy_preload/loader_patch"
require_relative "ar_lazy_preload/loader"
require_relative "ar_lazy_preload/context"
N1Loader::Loadable.prepend(N1Loader::ArLazyPreload::Loadable)
N1Loader::Preloader.prepend(N1Loader::ArLazyPreload::PreloaderPatch)
N1Loader::Loader.prepend(N1Loader::ArLazyPreload::LoaderPatch)
N1Loader::LoaderCollection.prepend(N1Loader::ArLazyPreload::LoaderCollectionPatch)
================================================
FILE: lib/n1_loader/core/loadable.rb
================================================
# frozen_string_literal: true
module N1Loader
# The module to be included to the class to define associated loaders.
#
# class Example
# include N1Loader::Loadable
#
# # with inline loader
# n1_optimized :something do
# def perform(elements)
# elements.each { |element| fulfill(element, element.calculate_something) }
# end
# end
#
# # with custom loader
# n1_optimized :something, MyLoader
# end
#
# # custom loader
# class MyLoader < N1Loader::Loader
# def perform(elements)
# elements.each { |element| fulfill(element, element.calculate_something) }
# end
# end
module Loadable
def n1_loaders
@n1_loaders ||= {}
end
def n1_loader(name)
n1_loaders[name]
end
def n1_bind_to(collection)
unless collection.is_a?(Array) && collection.any? do |obj|
obj == self || obj.equal?(self)
end
raise InvalidBinding,
"assigned collection should be array and include object"
end
@n1_binding = collection
end
def n1_bind_to?
!@n1_binding.nil?
end
def n1_loader_reload(name)
elements = @n1_binding || [self]
collection = LoaderCollection.new(self.class.n1_loaders[name], elements)
@n1_binding&.each { |el| el.n1_loaders[name] = collection if el.respond_to?(:n1_loaders) }
n1_loaders[name] = collection
end
def n1_clear_cache
@n1_binding = nil
self.class.n1_loaders.each_key do |name|
n1_loaders[name] = nil
end
end
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods # :nodoc:
def n1_loaders
@n1_loaders ||= superclass.respond_to?(:n1_loaders) ? superclass.n1_loaders.dup : {}
end
def n1_optimized(name, loader = nil, &block)
loader ||= LoaderBuilder.build(&block)
n1_loaders[name] = loader
define_method(name) do |reload: false, **args|
n1_loader_reload(name) if reload || n1_loader(name).nil?
n1_loader(name).with(**args).for(self)
end
end
end
end
end
================================================
FILE: lib/n1_loader/core/loader.rb
================================================
# frozen_string_literal: true
module N1Loader
# Loader that performs the loading.
#
# Subclasses must define +perform+ method that accepts single argument
# and returns hash where key is the element and value is what we want to load.
class Loader
prepend MonitorMixin
class << self
attr_reader :arguments
# Defines an argument that can be accessed within the loader.
#
# First defined argument will have the value of first passed argument,
# meaning the order is important.
#
# @param name [Symbol]
# @param opts [Hash]
# @option opts [Boolean] optional false by default
# @option opts [Proc] default
def argument(name, **opts)
opts[:optional] = true if opts[:default]
@arguments ||= []
define_method(name) do
args.fetch(name) { args[name] = opts[:default]&.call }
end
@arguments << opts.merge(name: name)
end
# Defines a custom cache key that is calculated for passed arguments.
def cache_key(&block)
define_method(:cache_key) do
check_arguments!
instance_exec(&block)
end
end
end
def initialize(elements, **args)
@elements = elements
@args = args
end
def for(element)
return unless loaded?
if loaded_by_identity.empty? && elements.any?
raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
end
return loaded_by_identity[element] if loaded_by_identity.key?(element)
return loaded_by_value[element] if loaded_by_value.key?(element)
raise NotLoaded, "The data was not preloaded for the given element"
end
def cache_key
check_arguments!
args.values.map(&:object_id)
end
private
attr_reader :elements, :args, :loaded_by_value, :loaded_by_identity
def check_missing_arguments!
return unless (arguments = self.class.arguments)
required_arguments = required_arguments(arguments)
return if required_arguments.all? { |argument| args.key?(argument) }
missing_arguments = required_arguments.reject { |argument| args.key?(argument) }
list = missing_arguments.map { |argument| ":#{argument}" }.join(", ")
raise MissingArgument, "Loader requires [#{list}] arguments but they are missing"
end
def required_arguments(args)
args.reject { |argument| argument[:optional] }
.map { |argument| argument[:name] }
end
def check_arguments!
check_missing_arguments!
check_invalid_arguments!
end
def check_invalid_arguments!
return unless (arguments = self.class.arguments)
args.each_key do |arg|
next if arguments.find { |argument| argument[:name] == arg }
raise InvalidArgument, "Loader doesn't define #{arg} argument"
end
end
def perform(_elements)
raise NotImplemented, "Subclasses have to implement the method"
end
def fulfill(element, value)
loaded_by_identity[element] = value
loaded_by_value[element] = value
end
def loaded?
return true if @already_loaded
synchronize { non_thread_safe_loading unless @already_loaded }
true
end
def non_thread_safe_loading # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
return if @already_loaded
check_arguments!
@loaded_by_identity = {}.compare_by_identity
@loaded_by_value = {}
if respond_to?(:single) && elements.size == 1
fulfill(elements.first, single(elements.first))
elsif elements.any?
perform(elements)
# propagate context to loaded objects only when it was set
if elements.first.respond_to?(:n1_bind_to?) && elements.first.n1_bind_to?
loaded_objects = loaded_by_identity.values.flatten
loaded_objects.each { |el| el.n1_bind_to(loaded_objects) if el.respond_to?(:n1_bind_to) }
end
end
@already_loaded = true
end
end
end
================================================
FILE: lib/n1_loader/core/loader_builder.rb
================================================
# frozen_string_literal: true
module N1Loader
# The class builds {N1Loader::Loader}
class LoaderBuilder
def self.build(&block)
Class.new(N1Loader::Loader) do
if block.arity == 1
define_method(:perform, &block)
else
class_eval(&block)
end
end
end
end
end
================================================
FILE: lib/n1_loader/core/loader_collection.rb
================================================
# frozen_string_literal: true
module N1Loader
# The class is used for storing collections of loaders for elements per set of arguments.
class LoaderCollection
attr_reader :loader_class, :elements
def initialize(loader_class, elements)
@loader_class = loader_class
@elements = elements
end
def with(**args)
loader = loader_class.new(elements, **args)
loaders[loader.cache_key] ||= loader
end
private
def loaders
@loaders ||= {}
end
end
end
================================================
FILE: lib/n1_loader/core/preloader.rb
================================================
# frozen_string_literal: true
module N1Loader
# Preloader that lazily preloads data to every element.
#
# It supports multiple keys.
#
# It supports elements that have different loaders under the same key.
# It will properly preload data to each of the element of the similar group.
class Preloader
attr_reader :elements
def initialize(elements)
@elements = elements
end
def preload(*keys)
keys.flatten(1).flat_map do |key|
elements
.group_by { |element| loader_class(element, key) }
.select { |loader_class, _| loader_class }
.map do |(loader_class, grouped_elements)|
loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)
grouped_elements.each { |grouped_element| grouped_element.n1_loaders[key] = loader_collection }
loader_collection
end
end
end
private
def loader_class(element, key)
element.class.respond_to?(:n1_loaders) && element.class.n1_loaders[key]
end
end
end
================================================
FILE: lib/n1_loader/goldiloader/context.rb
================================================
# frozen_string_literal: true
# Returns cached N1Loader::LoaderCollection from context for a loader.
# In case there is none yet, saves passed block to a cache.
Goldiloader::AutoIncludeContext.define_method :fetch_n1_loader_collection do |loader, &block|
(@n1_loader_collections ||= {})[loader] ||= block.call
end
================================================
FILE: lib/n1_loader/goldiloader/context_adapter.rb
================================================
# frozen_string_literal: true
module N1Loader
module Goldiloader
# Context adapter for injected N1Loader loaders.
class ContextAdapter
attr_reader :context
def initialize(context)
@context = context
end
# Trigger preloading for +association_name+ across all models in the context.
def try_preload_lazily(association_name)
perform_preloading(association_name) if context
end
# Initialize preloader for +association_name+ with context builder callback.
# The callback will be executed when records are loaded.
def perform_preloading(association_name)
context_setup = lambda { |records|
ar_records = records.flatten(1).select { |record| record.respond_to?(:auto_include_context=) }
::Goldiloader::AutoIncludeContext.register_models(ar_records) unless ar_records.empty?
}
N1Loader::Preloader.new(context.models, context_setup).preload(association_name)
end
end
end
end
================================================
FILE: lib/n1_loader/goldiloader/loadable.rb
================================================
# frozen_string_literal: true
module N1Loader
module Goldiloader
module Loadable # :nodoc:
def n1_loader(name)
return n1_loaders[name] if n1_loaders[name]
ContextAdapter.new(auto_include_context).try_preload_lazily(name) if respond_to?(:auto_include_context)
super
end
end
end
end
================================================
FILE: lib/n1_loader/goldiloader/loader.rb
================================================
# frozen_string_literal: true
# Raised when a single object without Goldiloader context support was passed to an isolated loader.
N1Loader::Loader::UnsupportedGoldiloader = Class.new(StandardError)
# Defines a singleton method that allows isolated loaders
# to use Goldiloader context without passing sibling records.
N1Loader::Loader.define_singleton_method(:for) do |element, **args|
# It is required to have a Goldiloader context supported
raise N1Loader::Loader::UnsupportedGoldiloader unless element.respond_to?(:auto_include_context)
context = element.auto_include_context
# Fetch or initialize loader from Goldiloader context
loader_collection = context.fetch_n1_loader_collection(self) do
context_setup = lambda { |records|
ar_records = records.flatten(1).select { |record| record.respond_to?(:auto_include_context=) }
::Goldiloader::AutoIncludeContext.register_models(ar_records) unless ar_records.empty?
}
N1Loader::LoaderCollection.new(self, context.models).tap do |collection|
collection.context_setup = context_setup
end
end
# Fetch value from loader
loader_collection.with(**args).for(element)
end
================================================
FILE: lib/n1_loader/goldiloader/loader_collection_patch.rb
================================================
# frozen_string_literal: true
module N1Loader
module Goldiloader
# A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.
module LoaderCollectionPatch
attr_accessor :context_setup
def with(**args)
result = super
result.context_setup = context_setup if context_setup && result.context_setup.nil?
result
end
end
end
end
================================================
FILE: lib/n1_loader/goldiloader/loader_patch.rb
================================================
# frozen_string_literal: true
module N1Loader
module Goldiloader
# A patch to {N1Loader::Loader} to setup lazy context lazily.
module LoaderPatch
attr_accessor :context_setup
def loaded?
return true if @already_loaded && @already_context
super
synchronize { non_thread_safe_context_setting unless @already_context }
true
end
def non_thread_safe_context_setting
return if @already_context
context_setup&.call(loaded_by_identity.values.flatten)
@already_context = true
end
end
end
end
================================================
FILE: lib/n1_loader/goldiloader/preloader_patch.rb
================================================
# frozen_string_literal: true
module N1Loader
module Goldiloader
# A patch to {N1Loader::Preloader} to setup lazy context lazily.
module PreloaderPatch
def initialize(elements, context_setup = nil)
super(elements)
@context_setup = context_setup
end
def preload(*keys)
super.each do |loader_collection|
loader_collection.context_setup = context_setup
end
end
private
attr_reader :context_setup
end
end
end
================================================
FILE: lib/n1_loader/goldiloader.rb
================================================
# frozen_string_literal: true
# Load core library
require_relative "active_record"
# Load integration dependency
require "goldiloader"
# Library integration
require_relative "goldiloader/loadable"
require_relative "goldiloader/context_adapter"
require_relative "goldiloader/loader_collection_patch"
require_relative "goldiloader/preloader_patch"
require_relative "goldiloader/loader_patch"
require_relative "goldiloader/loader"
require_relative "goldiloader/context"
N1Loader::Loadable.prepend(N1Loader::Goldiloader::Loadable)
N1Loader::Preloader.prepend(N1Loader::Goldiloader::PreloaderPatch)
N1Loader::Loader.prepend(N1Loader::Goldiloader::LoaderPatch)
N1Loader::LoaderCollection.prepend(N1Loader::Goldiloader::LoaderCollectionPatch)
================================================
FILE: lib/n1_loader/version.rb
================================================
# frozen_string_literal: true
module N1Loader
VERSION = "3.0.0"
end
================================================
FILE: lib/n1_loader.rb
================================================
# frozen_string_literal: true
require_relative "n1_loader/version"
require_relative "n1_loader/core/loader_builder"
require_relative "n1_loader/core/loader"
require_relative "n1_loader/core/loader_collection"
require_relative "n1_loader/core/loadable"
require_relative "n1_loader/core/preloader"
module N1Loader # :nodoc:
class Error < StandardError; end
class NotImplemented < Error; end
class NotLoaded < Error; end
class NotFilled < Error; end
class MissingArgument < Error; end
class InvalidArgument < Error; end
class InvalidBinding < Error; end
end
================================================
FILE: n1_loader.gemspec
================================================
# frozen_string_literal: true
require_relative "lib/n1_loader/version"
Gem::Specification.new do |spec|
spec.name = "n1_loader"
spec.version = N1Loader::VERSION
spec.authors = ["Evgeniy Demin"]
spec.email = ["lawliet.djez@gmail.com"]
spec.summary = "Loader to solve N+1 issue for good."
spec.homepage = "https://github.com/djezzzl/n1_loader"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.5.0"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/djezzzl/n1_loader"
spec.metadata["changelog_uri"] = "https://github.com/djezzzl/n1_loader/master/CHANGELOG.md"
spec.metadata["funding_uri"] = "https://opencollective.com/n1_loader#support"
spec.files = Dir["lib/**/*"]
spec.require_paths = ["lib"]
spec.add_runtime_dependency "mutex_m"
spec.add_development_dependency "activerecord", ">= 5"
spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
spec.add_development_dependency "db-query-matchers", "~> 0.11"
spec.add_development_dependency "goldiloader", ">= 3"
spec.add_development_dependency "graphql", "~> 2.0"
spec.add_development_dependency "rails", ">= 5"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
spec.add_development_dependency "rubocop", "~> 1.7"
spec.add_development_dependency "sqlite3", ">= 1.3"
end
================================================
FILE: spec/activerecord_spec.rb
================================================
# frozen_string_literal: true
RSpec.describe "N1Loader ActiveRecord integration" do
require_relative "../lib/n1_loader/active_record"
before do
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.drop_table(table, force: :cascade)
end
ActiveRecord::Schema.verbose = false
ActiveRecord::Schema.define(version: 1) do
create_table(:entities)
create_table(:companies) do |t|
t.belongs_to :entity
end
create_table(:employees) do |t|
t.belongs_to :company
end
create_table(:assignments) do |t|
t.belongs_to :employee
end
end
stub_const("Entity", Class.new(ActiveRecord::Base) do
self.table_name = :entities
has_one :company, class_name: "Company"
class << self
def name
"Entity"
end
def perform!
@count = count + 1
end
def count
@count || 0
end
end
n1_optimized :data do
def perform(elements)
Entity.perform!
elements.each { |element| fulfill(element, [element]) }
end
end
n1_optimized :with_arguments do
argument :something
def perform(elements)
Entity.perform!
elements.each { |element| fulfill(element, [element, something]) }
end
end
end)
stub_const("Company", Class.new(ActiveRecord::Base) do
self.table_name = :companies
belongs_to :entity, class_name: "Entity"
has_many :employees, class_name: "Employee"
class << self
def name
"Company"
end
def perform!
@count = count + 1
end
def count
@count || 0
end
end
n1_optimized :data do
def perform(elements)
Company.perform!
hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
elements.each { |element| fulfill(element, hash[element.entity_id]) }
end
end
n1_optimized :with_arguments do
argument :something
def perform(elements)
Company.perform!
hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
end
end
n1_optimized :with_question_mark? do
argument :something
def perform(elements)
Entity.perform!
elements.each { |element| fulfill(element, [element, something]) }
end
end
n1_optimized :employee_data do
def perform(elements)
Company.perform!
employees_hash = Employee.where(company_id: elements.map(&:id)).group_by(&:company_id)
elements.each { |element| fulfill(element, employees_hash[element.id] || []) }
end
end
end)
stub_const("Employee", Class.new(ActiveRecord::Base) do
self.table_name = :employees
belongs_to :company, class_name: "Company"
has_many :assignments, class_name: "Assignment"
class << self
def name
"Employee"
end
def perform!
@count = count + 1
end
def count
@count || 0
end
end
end)
stub_const("Assignment", Class.new(ActiveRecord::Base) do
self.table_name = :assignments
belongs_to :employee, class_name: "Employee"
class << self
def name
"Assignment"
end
def perform!
@count = count + 1
end
def count
@count || 0
end
end
end)
end
let(:loader) do
Class.new(N1Loader::Loader) do
def perform(elements)
elements.each { |element| fulfill(element, [element]) }
end
end
end
let(:object) { Entity.create! }
it "works" do
expect { object.data }.to change(Entity, :count).by(1)
expect { object.data }.not_to change(Entity, :count)
expect(object.data).to eq([object])
end
describe "loaded comparison" do
it "compares by value" do
instance = loader.new([object])
expect(instance.for(object)).to eq([object])
expect(instance.for(Entity.find(object.id))).to eq([object])
expect { instance.for(Entity.create!) }.to raise_error(N1Loader::NotLoaded)
end
end
describe "question mark support" do
it "works" do
expect do
Company.includes(:with_question_mark?).each do |company|
expect(company.with_question_mark?(something: "something")).to eq([company, "something"])
end
end.not_to raise_error
end
end
context "with preloader" do
let(:objects) { [Entity.create!, Entity.create!] }
it "works" do
expect { N1Loader::Preloader.new(objects).preload(:data) }.not_to change(Entity, :count)
expect do
objects.each do |object|
expect(object.data).to eq([object])
end
end.to change(Entity, :count).by(1)
end
end
context "with includes" do
let(:objects) { Entity.includes(:data) }
before do
Entity.create!
Entity.create!
end
it "works" do
expect do
objects.each do |object|
expect(object.data).to eq([object])
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(count: 1)
.and change(Entity, :count).by(1)
end
context "when preloading AR" do
let(:objects) { Entity.includes(:company) }
it "doesn't set context further for N1Loader" do
expect do
objects.each do |object|
expect(object.company.data).to eq(object)
end
.to make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(matching: /entities/, count: 2)
.and make_database_queries(count: 3)
end
end
end
context "with arguments" do
before { skip "unsupported by ArLazyPreload" if ar_lazy_preload_defined? }
let(:objects) { Entity.includes(:with_arguments) }
it "works" do
expect do
expect do
objects.each do |object|
object.with_arguments(something: "something")
end
end.to change(Entity, :count).by(1)
expect do
objects.each do |object|
object.with_arguments(something: "something")
end
end.not_to change(Entity, :count)
expect do
objects.each do |object|
object.with_arguments(something: "anything")
end
end.to change(Entity, :count).by(1)
objects.each do |object|
expect(object.with_arguments(something: "something")).to eq([object, "something"])
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(count: 1)
end
end
end
context "with nested includes" do
let(:objects) { Entity.includes(company: %i[entity data] + [{ employee_data: :assignments }]) }
before do
Company.create!(entity: Entity.create!, employees: [Employee.create!(assignments: [Assignment.create!])])
Company.create!(entity: Entity.create!, employees: [Employee.create!(assignments: [Assignment.create!])])
end
it "works" do
expect do
objects.each do |object|
expect(object.company.data).to eq(object)
end
end
.to make_database_queries(matching: /entities/, count: 2)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(matching: /employees/, count: 1)
.and make_database_queries(matching: /assignments/, count: 1)
.and make_database_queries(count: 5)
.and change(Company, :count).by(2)
objects.each do |object|
expect(object.company.employee_data).to eq(object.company.employees)
expect(object.company.employee_data.map(&:assignments)).to eq(object.company.employees.map(&:assignments))
end
end
context "with arguments" do
let(:objects) { Entity.includes(company: :with_arguments) }
before { skip "unsupported by ActiveRecord 6" if ar_version == 6 }
it "works" do
expect do
objects.each do |object|
expect(object.company.with_arguments(something: "something")).to eq([object, "something"])
end
end
.to make_database_queries(matching: /entities/, count: 2)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 3)
.and change(Company, :count).by(1)
end
end
end
context "with deep includes" do
let(:objects) { Company.includes(data: :company) }
before do
Company.create!(entity: Entity.create!)
Company.create!(entity: Entity.create!)
end
it "works" do
expect do
objects.each do |object|
expect(object.data.company.id).to eq(object.id)
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 2)
.and make_database_queries(count: 3)
.and change(Company, :count).by(1)
end
context "with arguments" do
let(:objects) { Company.includes(with_arguments: :company) }
it "doesn't work" do
expect do
objects.each do |object|
expect(object.with_arguments(something: "something").first.company.id).to eq(object.id)
end
end.to raise_error(N1Loader::ActiveRecord::InvalidPreloading)
end
end
end
end
================================================
FILE: spec/ar_lazy_preload_spec.rb
================================================
# frozen_string_literal: true
RSpec.describe "N1Loader AR Lazy Preload integration" do
require_relative "../lib/n1_loader/ar_lazy_preload"
ActiveSupport.on_load(:active_record) do
ActiveRecord::Base.include(ArLazyPreload::Base)
ActiveRecord::Relation.prepend(ArLazyPreload::Relation)
ActiveRecord::AssociationRelation.prepend(ArLazyPreload::AssociationRelation)
ActiveRecord::Relation::Merger.prepend(ArLazyPreload::Merger)
[
ActiveRecord::Associations::CollectionAssociation,
ActiveRecord::Associations::Association
].each { |klass| klass.prepend(ArLazyPreload::Association) }
ActiveRecord::Associations::CollectionAssociation.prepend(ArLazyPreload::CollectionAssociation)
ActiveRecord::Associations::CollectionProxy.prepend(ArLazyPreload::CollectionProxy)
ArLazyPreload::Preloader.patch_for_rails_7! if ActiveRecord::VERSION::MAJOR >= 7
end
before do
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.drop_table(table, force: :cascade)
end
ActiveRecord::Schema.verbose = false
ActiveRecord::Schema.define(version: 1) do
create_table(:entities)
create_table(:companies) do |t|
t.belongs_to :entity
end
end
stub_const("Entity", Class.new(ActiveRecord::Base) do
self.table_name = :entities
has_one :company, class_name: "Company"
class << self
def name
"Entity"
end
def perform!
@count = count + 1
end
def count
@count || 0
end
end
n1_optimized :data do
def perform(elements)
elements.first.class.perform!
elements.each { |element| fulfill(element, [element]) }
end
end
n1_optimized :with_arguments do
argument :something
def perform(elements)
Entity.perform!
elements.each { |element| fulfill(element, [element, something]) }
end
end
end)
stub_const("Company", Class.new(ActiveRecord::Base) do
self.table_name = :companies
belongs_to :entity, class_name: "Entity"
class << self
def name
"Company"
end
def perform!
@count = count + 1
end
def count
@count || 0
end
end
n1_optimized :data do
def perform(elements)
elements.first.class.perform!
hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
elements.each { |element| fulfill(element, hash[element.entity_id]) }
end
end
n1_optimized :with_arguments do
argument :something
def perform(elements)
Company.perform!
hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
end
end
n1_optimized :with_question_mark? do
argument :something
def perform(elements)
Entity.perform!
elements.each { |element| fulfill(element, [element, something]) }
end
end
end)
end
before do
Company.create!(entity: Entity.create!)
Company.create!(entity: Entity.create!)
end
describe "question mark support" do
it "works" do
expect do
Company.preload_associations_lazily.each do |company|
expect(company.with_question_mark?(something: "something")).to eq([company, "something"])
end
end.not_to raise_error
end
end
it "works" do
expect do
Company.preload_associations_lazily.all.map(&:data)
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 2)
.and change(Company, :count).by(1)
expect do
Entity.preload_associations_lazily.all.map(&:company).map(&:data)
end
.to make_database_queries(matching: /entities/, count: 2)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 3)
.and change(Company, :count).by(1)
expect do
Company.preload_associations_lazily.all.map(&:data).map(&:company)
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 2)
.and make_database_queries(count: 3)
.and change(Company, :count).by(1)
expect do
Company.lazy_preload(data: :company).map(&:data).map(&:company)
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 2)
.and make_database_queries(count: 3)
.and change(Company, :count).by(1)
end
context "with arguments" do
it "works" do
expect do
Company.preload_associations_lazily.all.each { |company| company.with_arguments(something: "something") }
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 2)
.and change(Company, :count).by(1)
expect do
Entity.preload_associations_lazily.all.each { |entity| entity.company.with_arguments(something: "something") }
end
.to make_database_queries(matching: /entities/, count: 2)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 3)
expect do
Company.preload_associations_lazily.each do |company|
company.with_arguments(something: "something").first.company
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 2)
.and make_database_queries(count: 3)
expect do
Company.lazy_preload(with_arguments: :company).each do |company|
company.with_arguments(something: "something").first.company
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 2)
.and make_database_queries(count: 3)
end
end
describe "isolated loaders" do
let(:loader) do
Class.new(N1Loader::Loader) do
argument :something
def perform(_companies)
Company.perform!
hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
end
end
end
it "works with ArLazyPreload context" do
companies = Company.preload_associations_lazily.order(:id).to_a
entity1 = Entity.first
entity2 = Entity.second
loaded1 = loader.for(companies.first, something: "tmp")
loaded2 = loader.for(companies.second, something: "tmp")
expect(loaded1).to eq([entity1, "tmp"])
expect(loaded2).to eq([entity2, "tmp"])
expect(loaded1[0].lazy_preload_context)
.to be_present
.and eq(loaded2[0].lazy_preload_context)
expect do
companies.map do |company|
loader.for(company, something: "something")
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(count: 1)
.and change(Company, :count).by(1)
expect do
companies.each do |company|
loader.for(company, something: "anything")
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(count: 1)
.and change(Company, :count).by(1)
end
context "when object does not have ArLazyPreload context" do
it "raises an error" do
expect do
loader.for("string", something: "something")
end.to raise_error(N1Loader::Loader::UnsupportedArLazyPreload)
expect do
loaded = loader.for(Company.first, something: "something")
expect(loaded[0].lazy_preload_context).to be_present
end.not_to raise_error
end
end
end
end
================================================
FILE: spec/goldiloader_spec.rb
================================================
# frozen_string_literal: true
RSpec.describe "N1Loader Goldiloader integration" do
require_relative "../lib/n1_loader/goldiloader"
before do
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.drop_table(table, force: :cascade)
end
ActiveRecord::Schema.verbose = false
ActiveRecord::Schema.define(version: 1) do
create_table(:entities)
create_table(:companies) do |t|
t.belongs_to :entity
end
end
stub_const("Entity", Class.new(ActiveRecord::Base) do
self.table_name = :entities
has_one :company, class_name: "Company"
class << self
def name
"Entity"
end
def perform!
@count = count + 1
end
def count
@count || 0
end
end
n1_optimized :data do
def perform(elements)
elements.first.class.perform!
elements.each { |element| fulfill(element, [element]) }
end
end
n1_optimized :with_arguments do
argument :something
def perform(elements)
Entity.perform!
elements.each { |element| fulfill(element, [element, something]) }
end
end
end)
stub_const("Company", Class.new(ActiveRecord::Base) do
self.table_name = :companies
belongs_to :entity, class_name: "Entity"
class << self
def name
"Company"
end
def perform!
@count = count + 1
end
def count
@count || 0
end
end
n1_optimized :data do
def perform(elements)
elements.first.class.perform!
hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
elements.each { |element| fulfill(element, hash[element.entity_id]) }
end
end
n1_optimized :with_arguments do
argument :something
def perform(elements)
Company.perform!
hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
end
end
n1_optimized :with_question_mark? do
argument :something
def perform(elements)
Entity.perform!
elements.each { |element| fulfill(element, [element, something]) }
end
end
end)
end
before do
Company.create!(entity: Entity.create!)
Company.create!(entity: Entity.create!)
end
describe "question mark support" do
it "works" do
expect do
Company.all.each do |company|
expect(company.with_question_mark?(something: "something")).to eq([company, "something"])
end
end.not_to raise_error
end
end
it "works" do
expect do
Company.all.map(&:data)
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 2)
.and change(Company, :count).by(1)
expect do
Entity.all.map(&:company).map(&:data)
end
.to make_database_queries(matching: /entities/, count: 2)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 3)
.and change(Company, :count).by(1)
expect do
Company.all.map(&:data).map(&:company)
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 2)
.and make_database_queries(count: 3)
.and change(Company, :count).by(1)
end
context "with arguments" do
it "works" do
expect do
Company.all.each { |company| company.with_arguments(something: "something") }
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 2)
.and change(Company, :count).by(1)
expect do
Entity.all.each { |entity| entity.company.with_arguments(something: "something") }
end
.to make_database_queries(matching: /entities/, count: 2)
.and make_database_queries(matching: /companies/, count: 1)
.and make_database_queries(count: 3)
expect do
Company.all.each do |company|
company.with_arguments(something: "something").first.company
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(matching: /companies/, count: 2)
.and make_database_queries(count: 3)
end
end
describe "isolated loaders" do
let(:loader) do
Class.new(N1Loader::Loader) do
argument :something
def perform(_companies)
Company.perform!
hash = Entity.where(id: elements.map(&:entity_id)).index_by(&:id)
elements.each { |element| fulfill(element, [hash[element.entity_id], something]) }
end
end
end
it "works with Goldiloader context" do
companies = Company.order(:id).to_a
entity1 = Entity.first
entity2 = Entity.second
loaded1 = loader.for(companies.first, something: "tmp")
loaded2 = loader.for(companies.second, something: "tmp")
expect(loaded1).to eq([entity1, "tmp"])
expect(loaded2).to eq([entity2, "tmp"])
expect(loaded1[0].auto_include_context)
.to be_present
.and eq(loaded2[0].auto_include_context)
expect do
companies.map do |company|
loader.for(company, something: "something")
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(count: 1)
.and change(Company, :count).by(1)
expect do
companies.each do |company|
loader.for(company, something: "anything")
end
end
.to make_database_queries(matching: /entities/, count: 1)
.and make_database_queries(count: 1)
.and change(Company, :count).by(1)
end
context "when object does not have Goldiloader context" do
it "raises an error" do
expect do
loader.for("string", something: "something")
end.to raise_error(N1Loader::Loader::UnsupportedGoldiloader)
expect do
loaded = loader.for(Company.first, something: "something")
expect(loaded[0].auto_include_context).to be_present
end.not_to raise_error
end
end
end
end
================================================
FILE: spec/n1_loader_spec.rb
================================================
# frozen_string_literal: true
RSpec.describe N1Loader do
let(:loader) do
Class.new(N1Loader::Loader) do
def perform(elements)
elements.each { |element| fulfill(element, [element]) }
end
end
end
let(:klass) do
custom_loader = loader
Class.new do
include N1Loader::Loadable
class << self
def perform!
@count = count + 1
end
def count
@count || 0
end
end
n1_optimized :inline do |elements|
elements.first.class.perform!
elements.each { |element| fulfill(element, [element]) }
end
n1_optimized :sleepy do |elements|
sleep(0.5)
elements.first.class.perform!
elements.each { |element| fulfill(element, [element]) }
end
n1_optimized :custom, custom_loader
n1_optimized :single_optimized do
def single(element)
[element]
end
def perform(_elements)
raise "unknown"
end
end
n1_optimized :missing_fulfill do
def perform(elements)
elements.group_by(&:itself)
end
end
n1_optimized :with_arguments do
argument :something
argument :anything
def perform(elements)
elements.first.class.perform!
elements.each do |element|
fulfill(element, [element, something, anything])
end
end
end
n1_optimized :with_optional_argument do
argument :something, optional: true
argument :anything
def perform(elements)
elements.first.class.perform!
elements.each do |element|
fulfill(element, [element, something, anything])
end
end
end
n1_optimized :with_default_argument do
argument :something, default: -> { [] }
argument :anything
def perform(elements)
elements.first.class.perform!
elements.each do |element|
fulfill(element, [element, something, anything])
end
end
end
n1_optimized :with_custom_arguments_key do
argument :something
argument :anything
cache_key { something + anything }
def perform(elements)
elements.first.class.perform!
elements.each do |element|
fulfill(element, [element, something, anything])
end
end
end
n1_optimized :with_question_mark? do
argument :something
def perform(elements)
elements.first.class.perform!
elements.each do |element|
fulfill(element, [element, something])
end
end
end
n1_optimized :new_objects do |elements|
elements.first.class.perform!
objects = elements.map { elements.first.class.new }
elements.each_with_index do |element, index|
fulfill(element, objects[index])
end
end
end
end
let(:child_klass) do
Class.new(klass) do
n1_optimized :child_something do |elements|
elements.first.class.perform!
elements.each do |element|
fulfill(element, [element])
end
end
end
end
let(:object) { klass.new }
let(:objects) { [klass.new, klass.new] }
it "works with unsupported objects" do
expect do
N1Loader::Preloader.new([object, 123]).preload(:with_arguments)
end.not_to raise_error(NoMethodError)
end
describe "thread-safety" do
it "is thread-safe" do
N1Loader::Preloader.new(objects).preload(:sleepy)
threads = []
10.times do
threads << Thread.new do
objects.each do |obj|
expect(obj.sleepy).to eq([obj])
end
end
end
threads.each(&:join)
expect(klass.count).to eq(1)
end
end
describe "error handling" do
it "raises the same error on the subsequent calls" do
faulty_klass = Class.new do
include N1Loader::Loadable
n1_optimized :faulty do |_|
raise StandardError, "Something went wrong"
end
end
faulty_object = faulty_klass.new
expect { faulty_object.faulty }.to raise_error(StandardError, "Something went wrong")
expect { faulty_object.faulty }.to raise_error(StandardError, "Something went wrong")
end
end
describe "loaded comparison" do
it "compares by identity first" do
instance = loader.new(objects)
expect(objects.first).to equal(objects.first)
expect(instance.for(objects.first)).to eq([objects.first])
expect { instance.for(object) }.to raise_error(N1Loader::NotLoaded)
end
it "falls back to equality comparison when no identity match" do
equal_klass = Struct.new(:id)
original = equal_klass.new(1)
equal_copy = equal_klass.new(1)
custom_loader = Class.new(N1Loader::Loader) do
def perform(elements)
elements.each { |element| fulfill(element, [element]) }
end
end
instance = custom_loader.new([original])
expect(original).not_to equal(equal_copy)
expect(instance.for(original)).to eq([original])
expect(instance.for(equal_copy)).to eq([original])
end
end
context "when fulfill was not used" do
it "throws error" do
expect { object.missing_fulfill }
.to raise_error(N1Loader::NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method")
end
end
describe "question mark support" do
it "works" do
expect { object.with_question_mark?(something: "something") }.not_to raise_error
expect(object.with_question_mark?(something: "something")).to eq([object, "something"])
end
end
describe "clear cache" do
it "works" do
expect { object.inline }.to change(klass, :count).by(1)
expect { object.inline }.not_to change(klass, :count)
object.n1_clear_cache
expect { object.inline }.to change(klass, :count).by(1)
expect { object.inline }.not_to change(klass, :count)
end
context "with parent loader" do
let(:object) { child_klass.new }
it "works" do
expect { object.inline }.to change(child_klass, :count).by(1)
expect { object.child_something }.to change(child_klass, :count).by(1)
expect { object.inline }.not_to change(child_klass, :count)
expect { object.child_something }.not_to change(child_klass, :count)
object.n1_clear_cache
expect { object.inline }.to change(child_klass, :count).by(1)
expect { object.child_something }.to change(child_klass, :count).by(1)
expect { object.inline }.not_to change(child_klass, :count)
expect { object.child_something }.not_to change(child_klass, :count)
end
end
context "with binding" do
it "clears binding so objects load independently after cache clear" do
objects.each { |obj| obj.n1_bind_to(objects) }
# Binding ensures the whole collection loads in one batch
expect { objects.first.inline }.to change(klass, :count).by(1)
expect { objects.last.inline }.not_to change(klass, :count)
objects.each(&:n1_clear_cache)
# After clearing cache and binding, each object loads independently
expect { objects.first.inline }.to change(klass, :count).by(1)
expect { objects.last.inline }.to change(klass, :count).by(1)
end
end
end
describe "arguments support" do
it "has to receive all arguments" do
expect { object.with_arguments }.to raise_error(N1Loader::MissingArgument)
expect { object.with_arguments(something: "something") }.to raise_error(N1Loader::MissingArgument)
expect { object.with_arguments("something") }.to raise_error(ArgumentError)
expect(object.with_arguments(something: "something",
anything: "anything")).to eq([object,
"something", "anything"])
end
it "supports optional arguments" do
expect { object.with_optional_argument }
.to raise_error(N1Loader::MissingArgument, "Loader requires [:anything] arguments but they are missing")
expect(object.with_optional_argument(anything: 2)).to eq([object, nil, 2])
expect(object.with_optional_argument(something: 1, anything: 2)).to eq([object, 1, 2])
expect { object.with_optional_argument(tmp: 1, anything: 2) }
.to raise_error(N1Loader::InvalidArgument, "Loader doesn't define tmp argument")
end
it "supports default arguments" do
expect { object.with_default_argument }
.to raise_error(N1Loader::MissingArgument, "Loader requires [:anything] arguments but they are missing")
expect(object.with_default_argument(anything: 2)).to eq([object, [], 2])
expect(object.with_default_argument(something: 1, anything: 2)).to eq([object, 1, 2])
expect { object.with_default_argument(tmp: 1, anything: 2) }
.to raise_error(N1Loader::InvalidArgument, "Loader doesn't define tmp argument")
end
it "can have custom arguments key" do
# The following two has the same result for custom key, so we only do one perform
expect { object.with_custom_arguments_key(something: 1, anything: 2) }.to change(klass, :count).by(1)
expect { object.with_custom_arguments_key(something: 2, anything: 1) }.not_to change(klass, :count)
expect { object.with_custom_arguments_key(something: 2, anything: 3) }.to change(klass, :count).by(1)
end
it "supports named arguments" do
expect do
object.with_custom_arguments_key
end.to raise_error(N1Loader::MissingArgument,
"Loader requires [:something, :anything] arguments but they are missing")
expect do
object.with_custom_arguments_key(something: "something")
end.to raise_error(N1Loader::MissingArgument,
"Loader requires [:anything] arguments but they are missing")
expect(object.with_custom_arguments_key(something: "something",
anything: "anything")).to eq([
object, "something", "anything"
])
end
it "supports falsey argument values" do
expect(object.with_default_argument(anything: 2)).to eq([object, [], 2]) # default value
expect(object.with_default_argument(something: false, anything: 2)).to eq([object, false, 2]) # false
expect(object.with_default_argument(something: nil, anything: 2)).to eq([object, nil, 2]) # nil
end
it "works with preloading" do
N1Loader::Preloader.new(objects).preload(:with_arguments)
expect do
objects.each do |object|
expect(object.with_arguments(something: "something",
anything: "anything")).to eq([object,
"something", "anything"])
end
end.to change(klass, :count).by(1)
end
it "caches based on arguments" do
N1Loader::Preloader.new(objects).preload(:with_arguments, :with_default_argument)
expect do
objects.each { |object| object.with_arguments(something: "something", anything: "anything") }
end.to change(klass, :count).by(1)
expect do
objects.each { |object| object.with_arguments(something: "something2", anything: "anything") }
end.to change(klass, :count).by(1)
expect do
objects.each { |object| object.with_arguments(something: "something", anything: "anything2") }
end.to change(klass, :count).by(1)
expect do
objects.each { |object| object.with_arguments(something: "something", anything: "anything") }
end.not_to change(klass, :count)
expect do
objects.each { |object| object.with_default_argument(something: false, anything: nil) }
end.to change(klass, :count).by(1)
expect do
objects.each { |object| object.with_default_argument(something: false, anything: nil) }
end.not_to change(klass, :count)
end
it "supports reloading" do
expect do
object.with_arguments(something: "something", anything: "anything")
end.to change(klass, :count).by(1)
expect do
object.with_arguments(something: "something", anything: "anything", reload: true)
end.to change(klass, :count).by(1)
expect do
object.with_arguments(something: "something", anything: "anything")
end.not_to change(klass, :count)
end
end
describe "optimization for single object" do
it "uses optimization" do
expect(object.single_optimized).to eq([object])
N1Loader::Preloader.new(objects).preload(:single_optimized)
expect { objects.map(&:single_optimized) }.to raise_error(StandardError, "unknown")
end
end
describe "isolated loaders" do
it "does not need injection" do
instance = loader.new(objects)
objects.each do |object|
expect(instance.for(object)).to eq([object])
end
end
it "checks that element was provided" do
instance = loader.new(objects)
objects.each do |object|
expect(instance.for(object)).to eq([object])
end
expect do
instance.for(object)
end.to raise_error(N1Loader::NotLoaded, "The data was not preloaded for the given element")
end
end
describe "reloading" do
context "with preloading" do
it "reloads cached data" do
N1Loader::Preloader.new(objects).preload(:inline)
expect { objects.map(&:inline) }.to change(klass, :count).by(1)
N1Loader::Preloader.new(objects).preload(:inline)
expect { objects.map(&:inline) }.to change(klass, :count).by(1)
expect { objects.map(&:inline) }.not_to change(klass, :count)
end
end
context "without preloading" do
it "reloads cached data" do
expect { object.inline }.to change(klass, :count).by(1)
expect { object.inline(reload: true) }.to change(klass, :count).by(1)
expect { object.inline(reload: false) }.not_to change(klass, :count)
expect { object.inline }.not_to change(klass, :count)
end
end
end
context "with custom loader" do
it "works" do
expect(object.custom).to eq([object])
end
end
context "without preloading" do
it "returns right data" do
expect(object.inline).to eq([object])
end
it "caches data" do
expect { object.inline }.to change(klass, :count).by(1)
expect { object.inline }.not_to change(klass, :count)
end
end
context "with preloading" do
it "returns right data" do
N1Loader::Preloader.new(objects).preload(:inline)
expect(objects.first.inline).to eq([objects.first])
expect(objects.last.inline).to eq([objects.last])
end
it "lazy loads data" do
expect { N1Loader::Preloader.new(objects).preload(:inline) }.not_to change(klass, :count)
expect { objects.map(&:inline) }.to change(klass, :count).by(1)
end
it "uses preloaded data" do
N1Loader::Preloader.new(objects).preload(:inline)
expect { objects.map(&:inline) }.to change(klass, :count).by(1)
expect { objects.map(&:inline) }.not_to change(klass, :count)
end
end
describe "n1_bind_to" do
it "raises error if invalid collection was passed" do
expect do
objects.each { |obj| obj.n1_bind_to([]) }
end.to raise_error N1Loader::InvalidBinding
expect do
objects.first.n1_bind_to([objects.last])
end.to raise_error N1Loader::InvalidBinding
end
it "loads all bound objects in a single batch" do
objects.each { |obj| obj.n1_bind_to(objects) }
expect { objects.map(&:inline) }.to change(klass, :count).by(1)
end
it "caches the result after the first load" do
objects.each { |obj| obj.n1_bind_to(objects) }
expect { objects.map(&:inline) }.to change(klass, :count).by(1)
expect { objects.map(&:inline) }.not_to change(klass, :count)
end
it "lazily loads when the first object is accessed" do
objects.each { |obj| obj.n1_bind_to(objects) }
expect { objects.first.inline }.to change(klass, :count).by(1)
expect { objects.last.inline }.not_to change(klass, :count)
end
it "propagates context to loaded objects" do
objects.each { |obj| obj.n1_bind_to(objects) }
expect do
objects.each(&:new_objects)
end.to change(klass, :count).by(1)
expect do
objects.map(&:new_objects).map(&:inline)
end.to change(klass, :count).by(1)
end
end
describe "with preloader" do
it "doesn't propagate context to loaded objects" do
N1Loader::Preloader.new(objects).preload(:new_objects)
expect do
objects.each(&:new_objects)
end.to change(klass, :count).by(1)
expect do
objects.map(&:new_objects).map(&:inline)
end.to change(klass, :count).by(2)
end
end
end
================================================
FILE: spec/spec_helper.rb
================================================
# frozen_string_literal: true
require "n1_loader"
require "db-query-matchers"
DBQueryMatchers.configure do |config|
config.schemaless = true
end
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
def ar_lazy_preload_defined?
defined?(ArLazyPreload)
end
def ar_version
ActiveRecord::VERSION::MAJOR
end
end
gitextract_b233zkyp/
├── .github/
│ ├── FUNDING.yml
│ ├── copilot-instructions.md
│ └── workflows/
│ ├── copilot-setup-steps.yml
│ ├── release.yml
│ ├── rubocop.yml
│ └── tests.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── activerecord-gemfiles/
│ ├── ar_5_latest.gemfile
│ ├── ar_6_latest.gemfile
│ ├── ar_7_latest.gemfile
│ └── ar_8_latest.gemfile
├── ar_lazy_preload-gemfiles/
│ ├── ar_lazy_preload_0.6.1.gemfile
│ └── ar_lazy_preload_master.gemfile
├── bin/
│ ├── console
│ └── setup
├── examples/
│ ├── active_record_integration.rb
│ ├── ar_lazy_integration.rb
│ ├── ar_lazy_integration_with_isolated_loader.rb
│ ├── arguments_support.rb
│ ├── context/
│ │ ├── service.rb
│ │ ├── setup_ar_lazy.rb
│ │ └── setup_database.rb
│ ├── core.rb
│ ├── goldiloader_integration.rb
│ ├── goldiloader_integration_with_isolated_loader.rb
│ ├── graphql.rb
│ ├── isolated_loader.rb
│ ├── lazy_loading.rb
│ ├── n1_bind_to.rb
│ ├── reloading.rb
│ ├── shared_loader.rb
│ └── single_case.rb
├── goldiloader-gemfiles/
│ └── goldiloader.gemfile
├── guides/
│ └── enhanced-activerecord.md
├── lib/
│ ├── n1_loader/
│ │ ├── active_record/
│ │ │ ├── associations_preloader.rb
│ │ │ ├── associations_preloader_v5.rb
│ │ │ ├── associations_preloader_v6.rb
│ │ │ ├── base.rb
│ │ │ ├── loader.rb
│ │ │ └── loader_collection.rb
│ │ ├── active_record.rb
│ │ ├── ar_lazy_preload/
│ │ │ ├── associated_context_builder.rb
│ │ │ ├── context.rb
│ │ │ ├── context_adapter.rb
│ │ │ ├── loadable.rb
│ │ │ ├── loader.rb
│ │ │ ├── loader_collection_patch.rb
│ │ │ ├── loader_patch.rb
│ │ │ └── preloader_patch.rb
│ │ ├── ar_lazy_preload.rb
│ │ ├── core/
│ │ │ ├── loadable.rb
│ │ │ ├── loader.rb
│ │ │ ├── loader_builder.rb
│ │ │ ├── loader_collection.rb
│ │ │ └── preloader.rb
│ │ ├── goldiloader/
│ │ │ ├── context.rb
│ │ │ ├── context_adapter.rb
│ │ │ ├── loadable.rb
│ │ │ ├── loader.rb
│ │ │ ├── loader_collection_patch.rb
│ │ │ ├── loader_patch.rb
│ │ │ └── preloader_patch.rb
│ │ ├── goldiloader.rb
│ │ └── version.rb
│ └── n1_loader.rb
├── n1_loader.gemspec
└── spec/
├── activerecord_spec.rb
├── ar_lazy_preload_spec.rb
├── goldiloader_spec.rb
├── n1_loader_spec.rb
└── spec_helper.rb
SYMBOL INDEX (233 symbols across 44 files)
FILE: examples/active_record_integration.rb
class User (line 7) | class User < ActiveRecord::Base
class Payment (line 20) | class Payment < ActiveRecord::Base
FILE: examples/ar_lazy_integration.rb
class User (line 8) | class User < ActiveRecord::Base
class Payment (line 21) | class Payment < ActiveRecord::Base
FILE: examples/ar_lazy_integration_with_isolated_loader.rb
class Loader (line 8) | class Loader < N1Loader::Loader
method perform (line 9) | def perform(users)
class User (line 19) | class User < ActiveRecord::Base
class Payment (line 23) | class Payment < ActiveRecord::Base
FILE: examples/arguments_support.rb
class User (line 7) | class User < ActiveRecord::Base
method perform (line 26) | def perform(users)
class Payment (line 43) | class Payment < ActiveRecord::Base
FILE: examples/context/service.rb
class Service (line 2) | class Service
method count (line 3) | def self.count
method increase! (line 7) | def self.increase!
method receive (line 11) | def self.receive(*users)
method single (line 17) | def self.single(user)
FILE: examples/context/setup_database.rb
function fill_database (line 19) | def fill_database
FILE: examples/core.rb
class User (line 8) | class User
method unoptimized_call (line 11) | def unoptimized_call
FILE: examples/goldiloader_integration.rb
class User (line 7) | class User < ActiveRecord::Base
class Payment (line 20) | class Payment < ActiveRecord::Base
FILE: examples/goldiloader_integration_with_isolated_loader.rb
class Loader (line 7) | class Loader < N1Loader::Loader
method perform (line 8) | def perform(users)
class User (line 18) | class User < ActiveRecord::Base
class Payment (line 22) | class Payment < ActiveRecord::Base
FILE: examples/graphql.rb
class User (line 9) | class User < ActiveRecord::Base
class Payment (line 22) | class Payment < ActiveRecord::Base
class UserType (line 38) | class UserType < GraphQL::Schema::Object
class QueryType (line 42) | class QueryType < GraphQL::Schema::Object
method users (line 45) | def users
class Schema (line 50) | class Schema < GraphQL::Schema
FILE: examples/isolated_loader.rb
class IsolatedLoader (line 3) | class IsolatedLoader < N1Loader::Loader
method perform (line 4) | def perform(elements)
FILE: examples/lazy_loading.rb
class User (line 6) | class User
FILE: examples/n1_bind_to.rb
class User (line 8) | class User
FILE: examples/reloading.rb
class User (line 5) | class User
FILE: examples/shared_loader.rb
class SharedLoader (line 6) | class SharedLoader < N1Loader::Loader
method perform (line 7) | def perform(objects)
class User (line 16) | class User
class Payment (line 22) | class Payment
FILE: examples/single_case.rb
class OptimizedLoader (line 6) | class OptimizedLoader < N1Loader::Loader
method perform (line 7) | def perform(objects)
method single (line 15) | def single(object)
class User (line 20) | class User
FILE: lib/n1_loader.rb
type N1Loader (line 11) | module N1Loader # :nodoc:
class Error (line 12) | class Error < StandardError; end
class NotImplemented (line 13) | class NotImplemented < Error; end
class NotLoaded (line 14) | class NotLoaded < Error; end
class NotFilled (line 15) | class NotFilled < Error; end
class MissingArgument (line 16) | class MissingArgument < Error; end
class InvalidArgument (line 17) | class InvalidArgument < Error; end
class InvalidBinding (line 18) | class InvalidBinding < Error; end
FILE: lib/n1_loader/active_record.rb
type N1Loader (line 9) | module N1Loader
type ActiveRecord (line 10) | module ActiveRecord
class InvalidPreloading (line 11) | class InvalidPreloading < N1Loader::Error; end
FILE: lib/n1_loader/active_record/associations_preloader.rb
type N1Loader (line 3) | module N1Loader
type ActiveRecord (line 4) | module ActiveRecord
type Associations (line 5) | module Associations
type Preloader (line 6) | module Preloader # :nodoc:
function options (line 8) | def options
function deprecated? (line 12) | def deprecated?
function through_reflection? (line 16) | def through_reflection?
function preloaders_for_reflection (line 21) | def preloaders_for_reflection(reflection, records)
function grouped_records (line 27) | def grouped_records # rubocop:disable Metrics/PerceivedComplexit...
FILE: lib/n1_loader/active_record/associations_preloader_v5.rb
type N1Loader (line 3) | module N1Loader
type ActiveRecord (line 4) | module ActiveRecord
type Associations (line 5) | module Associations
type Preloader (line 6) | module Preloader # :nodoc:
function options (line 8) | def options
function preloaders_for_one (line 13) | def preloaders_for_one(association, records, scope)
function grouped_records (line 25) | def grouped_records(association, records)
FILE: lib/n1_loader/active_record/associations_preloader_v6.rb
type N1Loader (line 3) | module N1Loader
type ActiveRecord (line 4) | module ActiveRecord
type Associations (line 5) | module Associations
type Preloader (line 6) | module Preloader # :nodoc:
function options (line 8) | def options
function preloaders_for_reflection (line 13) | def preloaders_for_reflection(reflection, records, scope)
function grouped_records (line 19) | def grouped_records(association, records, polymorphic_parent)
FILE: lib/n1_loader/active_record/base.rb
type N1Loader (line 3) | module N1Loader
type ActiveRecord (line 4) | module ActiveRecord
type Base (line 6) | module Base
function reload (line 12) | def reload(*)
FILE: lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
type N1Loader (line 3) | module N1Loader
type ArLazyPreload (line 4) | module ArLazyPreload
class AssociatedContextBuilder (line 6) | class AssociatedContextBuilder < ::ArLazyPreload::AssociatedContextB...
method initialize (line 9) | def initialize(parent_context:, association_name:, records:)
method perform (line 14) | def perform
FILE: lib/n1_loader/ar_lazy_preload/context_adapter.rb
type N1Loader (line 3) | module N1Loader
type ArLazyPreload (line 4) | module ArLazyPreload
class ContextAdapter (line 6) | class ContextAdapter
method initialize (line 11) | def initialize(context)
method try_preload_lazily (line 16) | def try_preload_lazily(association_name)
method perform_preloading (line 24) | def perform_preloading(association_name)
FILE: lib/n1_loader/ar_lazy_preload/loadable.rb
type N1Loader (line 3) | module N1Loader
type ArLazyPreload (line 4) | module ArLazyPreload
type Loadable (line 5) | module Loadable # :nodoc:
function n1_loader (line 6) | def n1_loader(name)
FILE: lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb
type N1Loader (line 3) | module N1Loader
type ArLazyPreload (line 4) | module ArLazyPreload
type LoaderCollectionPatch (line 6) | module LoaderCollectionPatch
function with (line 9) | def with(**args)
FILE: lib/n1_loader/ar_lazy_preload/loader_patch.rb
type N1Loader (line 3) | module N1Loader
type ArLazyPreload (line 4) | module ArLazyPreload
type LoaderPatch (line 6) | module LoaderPatch
function loaded? (line 9) | def loaded?
function non_thread_safe_context_setting (line 19) | def non_thread_safe_context_setting
FILE: lib/n1_loader/ar_lazy_preload/preloader_patch.rb
type N1Loader (line 3) | module N1Loader
type ArLazyPreload (line 4) | module ArLazyPreload
type PreloaderPatch (line 6) | module PreloaderPatch
function initialize (line 7) | def initialize(elements, context_setup = nil)
function preload (line 12) | def preload(*keys)
FILE: lib/n1_loader/core/loadable.rb
type N1Loader (line 3) | module N1Loader
type Loadable (line 26) | module Loadable
function n1_loaders (line 27) | def n1_loaders
function n1_loader (line 31) | def n1_loader(name)
function n1_bind_to (line 35) | def n1_bind_to(collection)
function n1_bind_to? (line 47) | def n1_bind_to?
function n1_loader_reload (line 51) | def n1_loader_reload(name)
function n1_clear_cache (line 59) | def n1_clear_cache
function included (line 66) | def self.included(base)
type ClassMethods (line 70) | module ClassMethods # :nodoc:
function n1_loaders (line 71) | def n1_loaders
function n1_optimized (line 75) | def n1_optimized(name, loader = nil, &block)
FILE: lib/n1_loader/core/loader.rb
type N1Loader (line 3) | module N1Loader
class Loader (line 8) | class Loader
method argument (line 23) | def argument(name, **opts)
method cache_key (line 36) | def cache_key(&block)
method initialize (line 44) | def initialize(elements, **args)
method for (line 49) | def for(element)
method cache_key (line 62) | def cache_key
method check_missing_arguments! (line 71) | def check_missing_arguments!
method required_arguments (line 85) | def required_arguments(args)
method check_arguments! (line 90) | def check_arguments!
method check_invalid_arguments! (line 95) | def check_invalid_arguments!
method perform (line 105) | def perform(_elements)
method fulfill (line 109) | def fulfill(element, value)
method loaded? (line 114) | def loaded?
method non_thread_safe_loading (line 122) | def non_thread_safe_loading # rubocop:disable Metrics/AbcSize, Metri...
FILE: lib/n1_loader/core/loader_builder.rb
type N1Loader (line 3) | module N1Loader
class LoaderBuilder (line 5) | class LoaderBuilder
method build (line 6) | def self.build(&block)
FILE: lib/n1_loader/core/loader_collection.rb
type N1Loader (line 3) | module N1Loader
class LoaderCollection (line 5) | class LoaderCollection
method initialize (line 8) | def initialize(loader_class, elements)
method with (line 13) | def with(**args)
method loaders (line 21) | def loaders
FILE: lib/n1_loader/core/preloader.rb
type N1Loader (line 3) | module N1Loader
class Preloader (line 10) | class Preloader
method initialize (line 13) | def initialize(elements)
method preload (line 17) | def preload(*keys)
method loader_class (line 32) | def loader_class(element, key)
FILE: lib/n1_loader/goldiloader/context_adapter.rb
type N1Loader (line 3) | module N1Loader
type Goldiloader (line 4) | module Goldiloader
class ContextAdapter (line 6) | class ContextAdapter
method initialize (line 9) | def initialize(context)
method try_preload_lazily (line 14) | def try_preload_lazily(association_name)
method perform_preloading (line 20) | def perform_preloading(association_name)
FILE: lib/n1_loader/goldiloader/loadable.rb
type N1Loader (line 3) | module N1Loader
type Goldiloader (line 4) | module Goldiloader
type Loadable (line 5) | module Loadable # :nodoc:
function n1_loader (line 6) | def n1_loader(name)
FILE: lib/n1_loader/goldiloader/loader_collection_patch.rb
type N1Loader (line 3) | module N1Loader
type Goldiloader (line 4) | module Goldiloader
type LoaderCollectionPatch (line 6) | module LoaderCollectionPatch
function with (line 9) | def with(**args)
FILE: lib/n1_loader/goldiloader/loader_patch.rb
type N1Loader (line 3) | module N1Loader
type Goldiloader (line 4) | module Goldiloader
type LoaderPatch (line 6) | module LoaderPatch
function loaded? (line 9) | def loaded?
function non_thread_safe_context_setting (line 19) | def non_thread_safe_context_setting
FILE: lib/n1_loader/goldiloader/preloader_patch.rb
type N1Loader (line 3) | module N1Loader
type Goldiloader (line 4) | module Goldiloader
type PreloaderPatch (line 6) | module PreloaderPatch
function initialize (line 7) | def initialize(elements, context_setup = nil)
function preload (line 12) | def preload(*keys)
FILE: lib/n1_loader/version.rb
type N1Loader (line 3) | module N1Loader
FILE: spec/activerecord_spec.rb
function name (line 32) | def name
function perform! (line 36) | def perform!
function count (line 40) | def count
function perform (line 46) | def perform(elements)
function perform (line 56) | def perform(elements)
function name (line 71) | def name
function perform! (line 75) | def perform!
function count (line 79) | def count
function perform (line 85) | def perform(elements)
function perform (line 96) | def perform(elements)
function perform (line 107) | def perform(elements)
function perform (line 115) | def perform(elements)
function name (line 130) | def name
function perform! (line 134) | def perform!
function count (line 138) | def count
function name (line 150) | def name
function perform! (line 154) | def perform!
function count (line 158) | def count
function perform (line 167) | def perform(elements)
FILE: spec/ar_lazy_preload_spec.rb
function name (line 44) | def name
function perform! (line 48) | def perform!
function count (line 52) | def count
function perform (line 58) | def perform(elements)
function perform (line 68) | def perform(elements)
function name (line 82) | def name
function perform! (line 86) | def perform!
function count (line 90) | def count
function perform (line 96) | def perform(elements)
function perform (line 107) | def perform(elements)
function perform (line 118) | def perform(elements)
function perform (line 218) | def perform(_companies)
FILE: spec/goldiloader_spec.rb
function name (line 26) | def name
function perform! (line 30) | def perform!
function count (line 34) | def count
function perform (line 40) | def perform(elements)
function perform (line 50) | def perform(elements)
function name (line 64) | def name
function perform! (line 68) | def perform!
function count (line 72) | def count
function perform (line 78) | def perform(elements)
function perform (line 89) | def perform(elements)
function perform (line 100) | def perform(elements)
function perform (line 183) | def perform(_companies)
FILE: spec/n1_loader_spec.rb
function perform (line 6) | def perform(elements)
function perform! (line 19) | def perform!
function count (line 23) | def count
function single (line 44) | def single(element)
function perform (line 48) | def perform(_elements)
function perform (line 54) | def perform(elements)
function perform (line 63) | def perform(elements)
function perform (line 76) | def perform(elements)
function perform (line 89) | def perform(elements)
function perform (line 104) | def perform(elements)
function perform (line 116) | def perform(elements)
function perform (line 211) | def perform(elements)
FILE: spec/spec_helper.rb
function ar_lazy_preload_defined? (line 21) | def ar_lazy_preload_defined?
function ar_version (line 25) | def ar_version
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (140K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 77,
"preview": "# Supported platforms for funding\nopen_collective: n1_loader\ngithub: djezzzl\n"
},
{
"path": ".github/copilot-instructions.md",
"chars": 292,
"preview": "# Copilot Instructions\n\nEvery PR must have no errors from:\n\n```\nbundle exec rubocop\nbundle exec rspec spec/n1_loader_spe"
},
{
"path": ".github/workflows/copilot-setup-steps.yml",
"chars": 774,
"preview": "name: Copilot Setup Steps\n\n# Automatically run the setup steps when they are changed to allow for easy validation, and\n#"
},
{
"path": ".github/workflows/release.yml",
"chars": 1716,
"preview": "name: Release\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: 'Version to release (e.g. 2.1.4)"
},
{
"path": ".github/workflows/rubocop.yml",
"chars": 388,
"preview": "name: Rubocop\n\non:\n pull_request:\n schedule:\n - cron: '0 0 * * 0'\n\njobs:\n rubocop:\n runs-on: ubuntu-latest\n\n "
},
{
"path": ".github/workflows/tests.yml",
"chars": 1946,
"preview": "name: RSpec tests\n\non:\n pull_request:\n schedule:\n - cron: '0 0 * * 0'\n\njobs:\n tests:\n runs-on: ubuntu-latest\n "
},
{
"path": ".gitignore",
"chars": 129,
"preview": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n/.idea\n.rspec_status\nGemfile.lock\n.ruby-version"
},
{
"path": ".rspec",
"chars": 53,
"preview": "--format documentation\n--color\n--require spec_helper\n"
},
{
"path": ".rubocop.yml",
"chars": 325,
"preview": "AllCops:\n TargetRubyVersion: 2.5\n Exclude:\n - examples/**/*\n - vendor/bundle/**/*\n\nStyle/StringLiterals:\n Enabl"
},
{
"path": "CHANGELOG.md",
"chars": 4819,
"preview": "## [3.0.0] - 2026/03/18\n\n- Add support of [Goldiloader](https://github.com/KentaaNL/goldiloader)\n\n## [2.2.1] - 2026/03/1"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5228,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "Gemfile",
"chars": 614,
"preview": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\n# Specify your gem's dependencies in n1_loader.gemspec\ngem"
},
{
"path": "LICENSE.txt",
"chars": 1088,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2021 TODO: Write your name\n\nPermission is hereby granted, free of charge, to any pe"
},
{
"path": "README.md",
"chars": 19022,
"preview": "# N1Loader\n\n[![Gem Version][3]][4]\n[![][11]][12]\n[![][13]][14]\n[![][9]][10]\n\nN1Loader is designed to provide a simple wa"
},
{
"path": "Rakefile",
"chars": 208,
"preview": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rspec/core/rake_task\"\n\nRSpec::Core::RakeTask.new(:sp"
},
{
"path": "activerecord-gemfiles/ar_5_latest.gemfile",
"chars": 91,
"preview": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 5\"\ngem \"concurrent-ruby\", \"= 1.3.4\"\n"
},
{
"path": "activerecord-gemfiles/ar_6_latest.gemfile",
"chars": 148,
"preview": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 6\"\ngem \"benchmark\"\ngem \"bigdecimal\"\ngem \"concurrent-ruby\", \"= 1.3"
},
{
"path": "activerecord-gemfiles/ar_7_latest.gemfile",
"chars": 149,
"preview": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 7\"\ngem \"benchmark\"\ngem \"bigdecimal\"\ngem \"concurrent-ruby\", \"= 1.3"
},
{
"path": "activerecord-gemfiles/ar_8_latest.gemfile",
"chars": 116,
"preview": "# frozen_string_literal: true\n\ngem \"activerecord\", \"~> 8\"\ngem \"benchmark\"\ngem \"bigdecimal\"\ngem \"sqlite3\", \"< 2.4.0\"\n"
},
{
"path": "ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile",
"chars": 64,
"preview": "# frozen_string_literal: true\n\ngem \"ar_lazy_preload\", \"= 0.6.1\"\n"
},
{
"path": "ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile",
"chars": 129,
"preview": "# frozen_string_literal: true\n\ngem \"ar_lazy_preload\", git: \"https://github.com/DmitryTsepelev/ar_lazy_preload\", branch: "
},
{
"path": "bin/console",
"chars": 374,
"preview": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"n1_loader\"\n\n# You can add fixtures a"
},
{
"path": "bin/setup",
"chars": 131,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\n\n# Do any other automated setup that you need "
},
{
"path": "examples/active_record_integration.rb",
"chars": 835,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/active_record\"\n\nrequire_relative 'context/setup_database'\n\nclass User "
},
{
"path": "examples/ar_lazy_integration.rb",
"chars": 813,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\n\nrequire_relative 'context/setup_ar_lazy'\nrequire_rel"
},
{
"path": "examples/ar_lazy_integration_with_isolated_loader.rb",
"chars": 880,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\n\nrequire_relative 'context/setup_ar_lazy'\nrequire_rel"
},
{
"path": "examples/arguments_support.rb",
"chars": 1736,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/active_record\"\n\nrequire_relative 'context/setup_database'\n\nclass User "
},
{
"path": "examples/context/service.rb",
"chars": 329,
"preview": "# 3rd party service, or database, or anything else that can perform in batches\nclass Service\n def self.count\n @count"
},
{
"path": "examples/context/setup_ar_lazy.rb",
"chars": 649,
"preview": "ActiveSupport.on_load(:active_record) do\n ActiveRecord::Base.include(ArLazyPreload::Base)\n\n ActiveRecord::Relation.pre"
},
{
"path": "examples/context/setup_database.rb",
"chars": 626,
"preview": "require \"sqlite3\"\n\nActiveRecord::Base.establish_connection(adapter: \"sqlite3\", database: \":memory:\")\nActiveRecord::Base."
},
{
"path": "examples/core.rb",
"chars": 1162,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3r"
},
{
"path": "examples/goldiloader_integration.rb",
"chars": 706,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/goldiloader\"\n\nrequire_relative \"context/setup_database\"\n\nclass User < "
},
{
"path": "examples/goldiloader_integration_with_isolated_loader.rb",
"chars": 757,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/goldiloader\"\n\nrequire_relative \"context/setup_database\"\n\nclass Loader "
},
{
"path": "examples/graphql.rb",
"chars": 1183,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader/ar_lazy_preload\"\nrequire 'graphql'\n\nrequire_relative 'context/setup_da"
},
{
"path": "examples/isolated_loader.rb",
"chars": 335,
"preview": "require 'n1_loader'\n\nclass IsolatedLoader < N1Loader::Loader\n def perform(elements)\n elements.each { |element| fulfi"
},
{
"path": "examples/lazy_loading.rb",
"chars": 607,
"preview": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3rd party service without N+1\ncla"
},
{
"path": "examples/n1_bind_to.rb",
"chars": 879,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\n\nrequire_relative 'context/service'\n\n# Class that wants to request 3r"
},
{
"path": "examples/reloading.rb",
"chars": 758,
"preview": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\nclass User\n include N1Loader::Loadable\n\n n1_optimized :optimi"
},
{
"path": "examples/shared_loader.rb",
"chars": 721,
"preview": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Loader that will be shared between multiple classes\nclass Sha"
},
{
"path": "examples/single_case.rb",
"chars": 706,
"preview": "require 'n1_loader'\n\nrequire_relative 'context/service'\n\n# Loader that will be shared between multiple classes\nclass Opt"
},
{
"path": "goldiloader-gemfiles/goldiloader.gemfile",
"chars": 49,
"preview": "# frozen_string_literal: true\n\ngem \"goldiloader\"\n"
},
{
"path": "guides/enhanced-activerecord.md",
"chars": 8264,
"preview": "# Enhanced ActiveRecord\n\n- Do you like `ActiveRecord` preloading?\n- How many times have you resolved your N+1 issues wit"
},
{
"path": "lib/n1_loader/active_record/associations_preloader.rb",
"chars": 1436,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ActiveRecord\n module Associations\n module Preloader # :n"
},
{
"path": "lib/n1_loader/active_record/associations_preloader_v5.rb",
"chars": 1185,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ActiveRecord\n module Associations\n module Preloader # :n"
},
{
"path": "lib/n1_loader/active_record/associations_preloader_v6.rb",
"chars": 929,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ActiveRecord\n module Associations\n module Preloader # :n"
},
{
"path": "lib/n1_loader/active_record/base.rb",
"chars": 340,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ActiveRecord\n # Extension module for ActiveRecord::Base\n m"
},
{
"path": "lib/n1_loader/active_record/loader.rb",
"chars": 155,
"preview": "# frozen_string_literal: true\n\nN1Loader::Loader.define_method :preloaded_records do\n @preloaded_records ||= loaded? && "
},
{
"path": "lib/n1_loader/active_record/loader_collection.rb",
"chars": 448,
"preview": "# frozen_string_literal: true\n\nN1Loader::LoaderCollection.define_method :preloaded_records do\n raise N1Loader::ActiveRe"
},
{
"path": "lib/n1_loader/active_record.rb",
"chars": 1060,
"preview": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"../n1_loader\"\n\n# Load integration dependency\nrequir"
},
{
"path": "lib/n1_loader/ar_lazy_preload/associated_context_builder.rb",
"chars": 705,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ArLazyPreload\n # Context builder for N1Loader\n class Assoc"
},
{
"path": "lib/n1_loader/ar_lazy_preload/context.rb",
"chars": 322,
"preview": "# frozen_string_literal: true\n\n# Returns cached N1Loader::LoaderCollection from context for a loader.\n# In case there is"
},
{
"path": "lib/n1_loader/ar_lazy_preload/context_adapter.rb",
"chars": 1105,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ArLazyPreload\n # Context adapter for injected N1Loader loader"
},
{
"path": "lib/n1_loader/ar_lazy_preload/loadable.rb",
"chars": 334,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ArLazyPreload\n module Loadable # :nodoc:\n def n1_loader("
},
{
"path": "lib/n1_loader/ar_lazy_preload/loader.rb",
"chars": 1396,
"preview": "# frozen_string_literal: true\n\n# Raised when a single object without ArLazyPreload context support was passed to an isol"
},
{
"path": "lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb",
"chars": 397,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ArLazyPreload\n # A patch to {N1Loader::LoaderCollection} to s"
},
{
"path": "lib/n1_loader/ar_lazy_preload/loader_patch.rb",
"chars": 595,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ArLazyPreload\n # A patch to {N1Loader::Loader} to setup lazy "
},
{
"path": "lib/n1_loader/ar_lazy_preload/preloader_patch.rb",
"chars": 502,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module ArLazyPreload\n # A patch to {N1Loader::Preloader} setup lazy "
},
{
"path": "lib/n1_loader/ar_lazy_preload.rb",
"chars": 858,
"preview": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"active_record\"\n\n# Load integration dependency\nrequi"
},
{
"path": "lib/n1_loader/core/loadable.rb",
"chars": 2167,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n # The module to be included to the class to define associated loaders.\n"
},
{
"path": "lib/n1_loader/core/loader.rb",
"chars": 4084,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n # Loader that performs the loading.\n #\n # Subclasses must define +per"
},
{
"path": "lib/n1_loader/core/loader_builder.rb",
"chars": 325,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n # The class builds {N1Loader::Loader}\n class LoaderBuilder\n def sel"
},
{
"path": "lib/n1_loader/core/loader_collection.rb",
"chars": 513,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n # The class is used for storing collections of loaders for elements per"
},
{
"path": "lib/n1_loader/core/preloader.rb",
"chars": 1063,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n # Preloader that lazily preloads data to every element.\n #\n # It supp"
},
{
"path": "lib/n1_loader/goldiloader/context.rb",
"chars": 317,
"preview": "# frozen_string_literal: true\n\n# Returns cached N1Loader::LoaderCollection from context for a loader.\n# In case there is"
},
{
"path": "lib/n1_loader/goldiloader/context_adapter.rb",
"chars": 1006,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module Goldiloader\n # Context adapter for injected N1Loader loaders."
},
{
"path": "lib/n1_loader/goldiloader/loadable.rb",
"chars": 332,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module Goldiloader\n module Loadable # :nodoc:\n def n1_loader(na"
},
{
"path": "lib/n1_loader/goldiloader/loader.rb",
"chars": 1168,
"preview": "# frozen_string_literal: true\n\n# Raised when a single object without Goldiloader context support was passed to an isolat"
},
{
"path": "lib/n1_loader/goldiloader/loader_collection_patch.rb",
"chars": 395,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module Goldiloader\n # A patch to {N1Loader::LoaderCollection} to set"
},
{
"path": "lib/n1_loader/goldiloader/loader_patch.rb",
"chars": 593,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module Goldiloader\n # A patch to {N1Loader::Loader} to setup lazy co"
},
{
"path": "lib/n1_loader/goldiloader/preloader_patch.rb",
"chars": 503,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n module Goldiloader\n # A patch to {N1Loader::Preloader} to setup lazy"
},
{
"path": "lib/n1_loader/goldiloader.rb",
"chars": 740,
"preview": "# frozen_string_literal: true\n\n# Load core library\nrequire_relative \"active_record\"\n\n# Load integration dependency\nrequi"
},
{
"path": "lib/n1_loader/version.rb",
"chars": 71,
"preview": "# frozen_string_literal: true\n\nmodule N1Loader\n VERSION = \"3.0.0\"\nend\n"
},
{
"path": "lib/n1_loader.rb",
"chars": 572,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"n1_loader/version\"\n\nrequire_relative \"n1_loader/core/loader_builder\"\nre"
},
{
"path": "n1_loader.gemspec",
"chars": 1448,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/n1_loader/version\"\n\nGem::Specification.new do |spec|\n spec.name "
},
{
"path": "spec/activerecord_spec.rb",
"chars": 9818,
"preview": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader ActiveRecord integration\" do\n require_relative \"../lib/n1_loade"
},
{
"path": "spec/ar_lazy_preload_spec.rb",
"chars": 8291,
"preview": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader AR Lazy Preload integration\" do\n require_relative \"../lib/n1_lo"
},
{
"path": "spec/goldiloader_spec.rb",
"chars": 6631,
"preview": "# frozen_string_literal: true\n\nRSpec.describe \"N1Loader Goldiloader integration\" do\n require_relative \"../lib/n1_loader"
},
{
"path": "spec/n1_loader_spec.rb",
"chars": 17212,
"preview": "# frozen_string_literal: true\n\nRSpec.describe N1Loader do\n let(:loader) do\n Class.new(N1Loader::Loader) do\n def"
},
{
"path": "spec/spec_helper.rb",
"chars": 593,
"preview": "# frozen_string_literal: true\n\nrequire \"n1_loader\"\nrequire \"db-query-matchers\"\n\nDBQueryMatchers.configure do |config|\n "
}
]
About this extraction
This page contains the full source code of the djezzzl/n1_loader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (126.5 KB), approximately 34.8k tokens, and a symbol index with 233 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.