Model or dataset
DmitryTsepelev/ar_lazy_preload avatar
DmitryTsepelev/ar_lazy_preload

ArLazyPreload: loading Rails associations only when the code actually touches them

Lazy loading associations for the ActiveRecord models

746 stars26 forksRubyMIT

At a glance

What is it?
ArLazyPreload replaces includes, eager_load and preload with lazy_preload, so associations are fetched in one batched query the first time a relation is used, which suits GraphQL resolvers where the selection set is not known in advance. The cost is a set of documented edge cases around size, includes and ActiveStorage.
Who is it for?
ArLazyPreload earns a place in a Rails application whose association list is decided at request time, most obviously a GraphQL server where the client picks the fields, and the auto_preload setting is a reasonable default only for a codebase you can afford to test end to end.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 166 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The association list is only known once the client asks

Rails gives you several built-in ways to avoid the N+1 problem, and the project's argument is not that those are wrong but that the list of associations to preload is sometimes not obvious in advance. The clearest case is GraphQL, where the fields requested arrive from the API client and the resolver has to inspect the selection set to work out which associations will be touched. The same applies to any API whose request body decides the shape of the data you need. ArLazyPreload is a gem that brings association lazy load functionality to Rails applications, and the mechanism is deliberately different from eager loading. Instead of loading a set of associations up front, the gem loads nothing until an association is called for the first time. At that moment it loads all the associated records for all records from the initial relation in a single query. The audience is Rails developers who have hit N+1 in a resolver and want to stop enumerating associations by hand.

One query when the relation is built, one more when the association is used

The usage example is short enough to read as a sequence of two statements. Building the relation issues one query and touches no association table:

ruby
users = User.lazy_preload(:posts).limit(10)  # => SELECT * FROM users LIMIT 10
users.map(&:first_name)

Reading the first names leaves the database alone beyond that first request. The moment the posts are read, the association triggers a second statement, and the important detail is the shape of the WHERE clause:

ruby
users.map(&:posts) # => SELECT * FROM posts WHERE user_id in (...)

The IN list, rather than one query per user, is the point of the whole gem. The set of parent identifiers comes from the initial relation, so the batch is complete even though nobody declared up front which associations would be needed. Note what the example does not do: it does not specify a limit on the posts query, and it does not show the parent relation being re-queried. Lazy preloading is a read-path optimisation, and the project's own advice elsewhere in the documentation is to separate read logic from write logic rather than expecting it to help on both.

Installing the gem and running the first example

Installation is a single line in the application's Gemfile, and the project's own wording is that adding it is all you need to do:

ruby
gem "ar_lazy_preload"

There is no initializer file to create and no configuration to set for the basic case, which is part of why the gem presents itself as a drop-in replacement for includes, eager_load and preload: you change the method name and the loading behaviour changes with it. A first real use therefore looks like this. Call lazy_preload with the association names on the relation, and the first access to any of those associations on any record in the relation issues a single batched query. Associations that are never touched cost nothing, which is the behaviour you want when a resolver returns only a subset of fields for a given request. The repository layout supports the claim of a real library rather than a snippet: there is a gemspec, a lib directory, a spec directory, a Rakefile and a CHANGELOG.md, and an Appraisals file with a gemfiles directory for testing against dependency versions.

auto_preload and the per-record escape hatch

If you would rather not name associations at all, the gem can be configured to load every association lazily on its own. The switch is a single assignment:

ruby
ArLazyPreload.config.auto_preload = true

After that there is no need to call lazy_preload on the association, since everything is loaded lazily. The trade-off is spelled out in a warning in the same document, and it is the one piece of advice worth reading twice. Enabling the setting makes the gem try to preload every association target throughout your app, and in any other gem that makes association target calls, which means previously working queries can start to fail and the project asks you to test most of your app paths when turning it on in an existing application. The escape hatch is per record rather than global. Calling skip_preload before any association method turns automatic preloading off for that one object:

ruby
users.first.skip_preload.posts # => SELECT * FROM posts WHERE user_id = ?

Notice the singular placeholder in that query. A skipped record falls back to a per-record lookup, so it is the exception you pay for rather than the default.

preload_associations_lazily covers the relation without naming associations

There is a third way to use the gem, described as an alternative for auto preloading. Instead of switching on a global configuration, you can call preload_associations_lazily on the relation itself, and the associations that get loaded are determined as the code reaches them:

ruby
posts = User.preload_associations_lazily.flat_map(&:posts)
# => SELECT * FROM users LIMIT 10
# => SELECT * FROM posts WHERE user_id in (...)

The two queries match the earlier example, but the relationship to the global setting differs. auto_preload is process-wide and applies to every ActiveRecord relation in the app, including relations created inside other gems. preload_associations_lazily is scoped to the relation you call it on, so it opts a single query into lazy behaviour without changing the semantics of unrelated code paths. For a large application that is the safer of the two options, and for a GraphQL resolver that only ever touches a few models it is usually enough on its own.

Four documented failure modes worth knowing before you adopt

The gem is unusually candid about where it breaks, and the list is the best argument for reading it before enabling anything. First, lazy preloading does not work for ActiveRecord below version 6 when includes is called earlier in the chain, because the earlier call has already resolved the association loading strategy. Second, calling size on an association prevents lazy preloading entirely, since size performs a SELECT COUNT request instead of loading the association when it has not been loaded yet. Third, lazy preloading for ActiveStorage variants does not work automatically, because of the way it is implemented, and the manual workaround is to call with_all_variant_records or with_attached_#{attachment_name} on the association. Fourth, lazy preloading does not work for association object creation methods, which is why the project advises separating write logic from read logic and using lazy preloading for reads only. Each of these is a shape of code rather than a rare condition, and two of them, size and creation methods, are easy to write by accident.

Where N1Loader takes over the same problem

The companion gem named in the documentation is N1Loader, and the division of labour is specific enough to be worth quoting. N1Loader complements ArLazyPreload for batched lazy loading in places where regular ActiveRecord associations are not available, for example computed values, custom loaders, service and API calls, and GraphQL fields. ArLazyPreload can stay focused on association loading while N1Loader handles the remaining N+1-prone derived data. That is the real difference in approach. ArLazyPreload works through the association proxy, so it is bound by what ActiveRecord exposes as an association, which is exactly why the ActiveStorage variant gap exists. A batching loader that treats a computed value or a custom loader as a first-class thing to batch is reaching a different set of code paths. The cost of running both is one more gem in the bundle and one more set of conventions to learn, and the benefit shows up in the parts of a GraphQL schema that resolve derived data rather than plain columns.

A gem with no tagged releases and a changelog instead

The repository publishes no GitHub releases, so version history lives in CHANGELOG.md rather than on the releases page, and the last push to the repository was on 2026-05-10. The gem is MIT licensed and available as open source under those terms, which removes licensing friction for a library that ends up in a commercial Rails application, and the README notes it was initially sponsored by Evil Martians. The badge row points at a build workflow running rspec, a coveralls coverage status for the master branch, a codeclimate maintainability badge, and separate performance and memory benchmark workflows, so the project's own claims about speed are backed by runnable comparisons rather than prose alone. The absence of tagged releases is the practical constraint. A team that wants to pin a known-good version has to read the changelog and the gemspec rather than pick a release tag, and behaviour changes between versions are something you confirm by reading that file.

Editorial conclusion

ArLazyPreload earns a place in a Rails application whose association list is decided at request time, most obviously a GraphQL server where the client picks the fields, and the auto_preload setting is a reasonable default only for a codebase you can afford to test end to end. It is the wrong tool for a codebase that leans on association size, that mixes includes into the same request on ActiveRecord below version 6, or that reads ActiveStorage variants often, because each of those paths is a documented failure rather than an edge case nobody noticed. Verify first that your target version is supported against the four gotchas in the project's own usage notes, and check the CHANGELOG.md in the repository, since the project publishes no GitHub releases and the last push was on 2026-05-10, so version-by-version behaviour changes have to be read rather than discovered from release notes.

Frequently asked questions

How do I install ArLazyPreload in a Rails app?

Add the gem to the application's Gemfile. The project states that this is the only change needed, and the usage examples then use lazy_preload on the relation instead of includes, eager_load or preload.

What is the difference between lazy_preload and includes in ArLazyPreload?

With lazy_preload nothing is loaded until an association is called for the first time, at which point all the associated records for all records from the initial relation are loaded in a single query. With includes the association list has to be known up front.

Why does ArLazyPreload not preload when I call size on an association?

The size method performs a SELECT COUNT request instead of loading the association when the association has not been loaded yet, so no lazy preload is triggered. The project links the relevant issue and an article explaining size, length and count.

Can I turn off auto preloading for a single record?

Yes. Calling skip_preload before any association method turns automatic preloading off for that record, and the association is then fetched with a per-record query such as SELECT * FROM posts WHERE user_id = ?.

Does ArLazyPreload work with ActiveStorage variants?

Not automatically, because of the way it is implemented. You can preload the variants manually by calling with_all_variant_records or with_attached_#{attachment_name} on the association.

Official sources

  1. DmitryTsepelev/ar_lazy_preload on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dmitrytsepelev-ar-lazy-preload.svg)](https://hysenlabs.com/projects/dmitrytsepelev-ar-lazy-preload)

Was this useful?