Goldiloader

repository·master·Indexed 23 days ago

https://github.com/salsify/goldiloader

An ActiveRecord extension for Rails applications that provides automatic eager loading to solve the N+1 query problem. It detects when associations are accessed uniformly across a collection and loads them in a single batch query. Supports Rails 7.2+ and Ruby 3.2+, with additional features like custom preloads via `goldiload` and granular control to disable eager loading at the query, association, or global level.

Tokens
2.2K
Snippets
3
Records
15
Agent score
82%

What's inside goldiloader

  1. How Goldiloader automatic eager loading works

    master

    Goldiloader automatically eager loads ActiveRecord associations when they are first accessed. It assumes that if you access an association on one model in a collection, you will likely do so for all of them.

    When the first model in a loaded collection traverses an association, Goldiloader executes a single query (using IN (...)) to load that association for all models in the current context, preventing N+1 queries.

    Example Behavior:

    # Without Goldiloader:
    # blogs.each { |blog| blog.posts.to_a }
    # SELECT * FROM posts WHERE blog_id = 1
    # SELECT * FROM posts WHERE blog_id = 2 ... (N queries)
    
    # With Goldiloader:
    # blogs.each { |blog| blog.posts.to_a }
    # SELECT * FROM posts WHERE blog_id IN (1,2,3,4,5) (1 query)
  2. Limitations of automatic eager loading

    master

    Goldiloader has certain limitations inherited from ActiveRecord's eager loading mechanism:

    1. has_one with SQL limits

    Do not use automatic eager loading for has_one associations that rely on a LIMIT 1 to return a single record. Eager loading converts the query to an IN (...) clause, which removes the limit and may fetch all associated records, causing performance issues.

    2. Associations with specific options

    Automatic eager loading is automatically disabled for associations that use:

    • limit
    • offset
    • finder_sql

    Workaround: You can use Custom Preloads to handle these cases.

  3. Install Goldiloader

    master

    To use Goldiloader in your Rails application, add it to your Gemfile and run bundle install.

    Requirements:

    • Rails 7.2+
    • Ruby 3.2+

    Note: For older versions of Rails/Ruby, refer to the specific release branches (e.g., release-5.x, release-4.x, etc.) in the repository.

    # Gemfile
    gem 'goldiloader'
    $ bundle

    Or install via gem directly:

    $ gem install goldiloader
  4. Disable automatic eager loading

    master

    You can disable automatic eager loading at three different granularities:

    1. For a specific query

    Use the auto_include(false) query scope method on an ActiveRecord relation.

    2. For a specific association

    Customize the association's scope in the model definition.

    3. Globally (Thread-local)

    Disable it for all threads using Goldiloader.globally_enabled = false in an initializer. You can then selectively enable it for specific code blocks using Goldiloader.enabled { ... } or Goldiloader.disabled { ... }.

    # Disable for a specific query
    Blog.order(:name).auto_include(false)
    
    # Disable for a specific association
    class Blog < ActiveRecord::Base
      has_many :posts, -> { auto_include(false) }
    end
    
    # Disable globally (in config/initializers/goldiloader.rb)
    Goldiloader.globally_enabled = false
    
    # Selectively enable/disable for a block (thread-local)
    Goldiloader.enabled do
      # Automatic eager loading is enabled for the current thread
    end
    
    Goldiloader.disabled do
      # Automatic eager loading is disabled for the current thread
    end
  5. Understand which associations are eligible for automatic eager loading

    master

    Goldiloader automatically determines if an association can be eager loaded based on several criteria. An association is considered eager_loadable? if:

    1. No Scoping Options: Associations without any scoping options (like where, order, etc.) are always eager loadable.
    2. Simple Scopes: If a scope exists, it must not have an arity greater than 0 (meaning it can't take arguments that would require per-instance evaluation).
    3. No Limits or Offsets: The scope cannot contain limit or offset calls.
    4. HasOne Constraints: For has_one associations, the scope cannot contain an order clause.

    Additionally, Goldiloader will only auto-include associations that do not have in-memory changes (i.e., they are not new_record?, changed?, or destroyed?), because the Rails association Preloader can clobber in-memory changes.

  6. Define custom preloads with `goldiload`

    master

    You can define custom preloads for non-association data (like aggregations, external API calls, or complex SQL) using the goldiload method.

    Requirements:

    • The block must return a Hash where the keys are the identifiers used for the lookup.
    • The block should not reference self (the instance) to avoid breaking the internal caching mechanism.

    Single Key Lookup

    Use goldiload to fetch data based on a single ID (e.g., blog_id).

    Custom Key Lookup

    Pass a key: option to specify which attribute(s) to use for the lookup.

    Multi-key Lookup

    Pass an array to the key: option to perform lookups based on multiple columns (e.g., [:organizer_id, :room_id]).

  7. Use the `fully_load` association option

    master

    Some ActiveRecord methods (like exists?, empty?, size, first, last) can behave differently depending on whether an association is already loaded in memory or if they trigger a new SQL query.

    If you want to ensure that calling these methods triggers Goldiloader's automatic eager loading instead of executing a single-record SQL check, use the fully_load: true option on the association.

  8. Control Goldiloader's enabled status

    master

    Goldiloader provides a way to enable or disable automatic eager loading globally or within specific blocks of code.

    • Global Status: Use Goldiloader.globally_enabled = true/false to set the process-wide default. By default, it is set to true.
    • Thread-local Status: Setting Goldiloader.enabled = true/false sets the status for the current thread. This is useful for isolating changes to specific requests or background jobs.
    • Scoped Control: Use the Goldiloader.enabled { ... } or Goldiloader.disabled { ... } methods to temporarily change the status for the duration of a block. This is the recommended way to disable eager loading for specific operations without affecting the rest of the application.
  9. Define custom preloads with `preloaded`

    master

    The preloaded method allows you to manually define how data should be preloaded for a specific model and cache. This is useful when standard association preloading is insufficient.

    To use it, provide the model class, a cache_name to identify the preload, and a key (or an array of keys) to extract from the records. The block passed to preloaded is executed in the context of the model's class, receiving the collected ids as an argument. The block should return a hash where the keys are the values extracted from the records and the values are the preloaded data.

    Note: The block is executed using instance_exec on the model class to prevent accidental references to specific model instances, ensuring the logic operates at the class level.

  10. Configure default eager loading behavior for a model

    master

    You can control whether associations are automatically fully loaded by setting the default_fully_load class attribute on your ActiveRecord model. By default, this is set to false.

    When default_fully_load is true, methods like .size, .ids, and .empty? on collection associations will trigger a full load of the association to ensure the collection is complete.

  11. Use goldiload to wrap code for manual preloading

    master

    The goldiload method allows you to manually wrap a block of code to manage preloading within a specific context. This is useful for fine-grained control over when and how models are registered for automatic eager loading. It accepts an optional cache_name and a key (defaults to the class's primary key).

    # Example usage of goldiload
    User.goldiload do
      # Code that triggers association loading
      users = User.limit(5).to_a
      users.each { |u| u.profile }
    end