n1_loader 0.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +56 -14
- data/CHANGELOG.md +20 -2
- data/Gemfile +9 -0
- data/README.md +199 -39
- data/activerecord-gemfiles/ar_5_latest.gemfile +3 -0
- data/{gemfiles → activerecord-gemfiles}/ar_6_latest.gemfile +0 -4
- data/ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile +3 -0
- data/ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile +3 -0
- data/lib/n1_loader/active_record/associations_preloader_v5.rb +39 -0
- data/lib/n1_loader/active_record/{associations_preloader.rb → associations_preloader_v6.rb} +0 -0
- data/lib/n1_loader/active_record/loader.rb +5 -0
- data/lib/n1_loader/active_record/loader_collection.rb +9 -0
- data/lib/n1_loader/active_record.rb +18 -1
- data/lib/n1_loader/ar_lazy_preload/associated_context_builder.rb +8 -1
- data/lib/n1_loader/ar_lazy_preload/context_adapter.rb +9 -6
- data/lib/n1_loader/ar_lazy_preload/loadable.rb +3 -3
- data/lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb +18 -0
- data/lib/n1_loader/ar_lazy_preload/loader_patch.rb +20 -0
- data/lib/n1_loader/ar_lazy_preload/preloader_patch.rb +23 -0
- data/lib/n1_loader/ar_lazy_preload.rb +9 -0
- data/lib/n1_loader/core/loadable.rb +24 -17
- data/lib/n1_loader/core/loader.rb +62 -12
- data/lib/n1_loader/core/loader_collection.rb +25 -0
- data/lib/n1_loader/core/preloader.rb +3 -3
- data/lib/n1_loader/version.rb +1 -1
- data/lib/n1_loader.rb +3 -0
- data/n1_loader.gemspec +4 -4
- metadata +27 -17
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: cc47894a27d425f70a38dc43b9b40053d823f4815fe8e135a7cb1cf5afbde0f3
         | 
| 4 | 
            +
              data.tar.gz: 132d5851c19763db2f1910ebeeb6a368437aba6d3340c2c69ad2ffe348791011
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 945258069b57bc7cd0fe26416411322886d734db0ed4eead1c88a3acced70bdb14ffc92eb9403e5d346c0853d64dc09e5afb8039a9026c9b4c09a1cc6f5fc9e8
         | 
| 7 | 
            +
              data.tar.gz: 036e5a8a4b2d63bed2aee558164fdd042ac8c0c68b0ecdb1d5ec9f64231ca9f6841dc56cd6cdc63b87fdb9bf964e4f42089f89d9783cf2513cf81243a473150a
         | 
    
        data/.circleci/config.yml
    CHANGED
    
    | @@ -11,8 +11,8 @@ executors: | |
| 11 11 | 
             
                docker:
         | 
| 12 12 | 
             
                  - image: circleci/ruby:<< parameters.tag >>
         | 
| 13 13 | 
             
                environment:
         | 
| 14 | 
            -
                   | 
| 15 | 
            -
                   | 
| 14 | 
            +
                  BUNDLE_JOBS: 4
         | 
| 15 | 
            +
                  BUNDLE_RETRY: 3
         | 
| 16 16 | 
             
                working_directory: ~/n1_loader
         | 
| 17 17 |  | 
| 18 18 | 
             
            jobs:
         | 
| @@ -29,28 +29,49 @@ jobs: | |
| 29 29 | 
             
                parameters:
         | 
| 30 30 | 
             
                  ruby-version:
         | 
| 31 31 | 
             
                    type: string
         | 
| 32 | 
            -
                  gemfile:
         | 
| 32 | 
            +
                  activerecord-gemfile:
         | 
| 33 33 | 
             
                    type: string
         | 
| 34 | 
            +
                  ar_lazy_preload-gemfile:
         | 
| 35 | 
            +
                    type: string
         | 
| 36 | 
            +
                environment:
         | 
| 37 | 
            +
                  ACTIVERECORD_GEMFILE: << parameters.activerecord-gemfile >>
         | 
| 38 | 
            +
                  AR_LAZY_PRELOAD_GEMFILE: << parameters.ar_lazy_preload-gemfile >>
         | 
| 34 39 | 
             
                executor:
         | 
| 35 40 | 
             
                  name: ruby
         | 
| 36 41 | 
             
                  tag: << parameters.ruby-version >>
         | 
| 37 42 | 
             
                steps:
         | 
| 38 43 | 
             
                  - attach_workspace:
         | 
| 39 44 | 
             
                      at: ~/n1_loader
         | 
| 40 | 
            -
                  - run:
         | 
| 41 | 
            -
                      name: Use << parameters.gemfile >> as the Gemfile
         | 
| 42 | 
            -
                      command: bundle config --global gemfile << parameters.gemfile >>
         | 
| 43 45 | 
             
                  - run:
         | 
| 44 46 | 
             
                      name: Install the gems specified by the Gemfile
         | 
| 45 47 | 
             
                      command: bundle install
         | 
| 46 48 | 
             
                  - run:
         | 
| 47 | 
            -
                      name: Run RSpec
         | 
| 49 | 
            +
                      name: Run Core RSpec
         | 
| 48 50 | 
             
                      command: |
         | 
| 49 51 | 
             
                        bundle exec rspec --profile 10 \
         | 
| 50 52 | 
             
                                          --format RspecJunitFormatter \
         | 
| 51 | 
            -
                                          --out test_results/ | 
| 53 | 
            +
                                          --out test_results/core.xml \
         | 
| 52 54 | 
             
                                          --format progress \
         | 
| 53 | 
            -
                                           | 
| 55 | 
            +
                                          spec/n1_loader_spec.rb
         | 
| 56 | 
            +
                  - run:
         | 
| 57 | 
            +
                      name: Run ActiveRecord integration RSpec
         | 
| 58 | 
            +
                      command: |
         | 
| 59 | 
            +
                        bundle exec rspec --profile 10 \
         | 
| 60 | 
            +
                                          --format RspecJunitFormatter \
         | 
| 61 | 
            +
                                          --out test_results/activerecord-integration.xml \
         | 
| 62 | 
            +
                                          --format progress \
         | 
| 63 | 
            +
                                          spec/n1_loader_spec.rb \
         | 
| 64 | 
            +
                                          spec/activerecord_spec.rb
         | 
| 65 | 
            +
                  - run:
         | 
| 66 | 
            +
                      name: Run ActiveRecord integration RSpec
         | 
| 67 | 
            +
                      command: |
         | 
| 68 | 
            +
                        bundle exec rspec --profile 10 \
         | 
| 69 | 
            +
                                          --format RspecJunitFormatter \
         | 
| 70 | 
            +
                                          --out test_results/ar-lazy-preload-integration.xml \
         | 
| 71 | 
            +
                                          --format progress \
         | 
| 72 | 
            +
                                          spec/n1_loader_spec.rb \
         | 
| 73 | 
            +
                                          spec/activerecord_spec.rb \
         | 
| 74 | 
            +
                                          spec/ar_lazy_preload_spec.rb
         | 
| 54 75 | 
             
                  - store_test_results:
         | 
| 55 76 | 
             
                      path: test_results
         | 
| 56 77 |  | 
| @@ -80,15 +101,36 @@ workflows: | |
| 80 101 | 
             
                        parameters:
         | 
| 81 102 | 
             
                          ruby-version: [
         | 
| 82 103 | 
             
                            "2.5",
         | 
| 83 | 
            -
                            "2.6",
         | 
| 84 104 | 
             
                            "2.7",
         | 
| 85 | 
            -
                            "3.0",
         | 
| 86 105 | 
             
                            "latest"
         | 
| 87 106 | 
             
                          ]
         | 
| 88 | 
            -
                          gemfile: [
         | 
| 89 | 
            -
                            " | 
| 107 | 
            +
                          activerecord-gemfile: [
         | 
| 108 | 
            +
                            "ar_5_latest",
         | 
| 109 | 
            +
                            "ar_6_latest"
         | 
| 90 110 | 
             
                          ]
         | 
| 91 | 
            -
             | 
| 111 | 
            +
                          ar_lazy_preload-gemfile: [
         | 
| 112 | 
            +
                            "ar_lazy_preload_0.6.1",
         | 
| 113 | 
            +
                            "ar_lazy_preload_master"
         | 
| 114 | 
            +
                          ]
         | 
| 115 | 
            +
                        exclude:
         | 
| 116 | 
            +
                          # Ruby 2.5 and AR Lazy Preload 1+
         | 
| 117 | 
            +
                          - ruby-version: "2.5"
         | 
| 118 | 
            +
                            activerecord-gemfile: "ar_5_latest"
         | 
| 119 | 
            +
                            ar_lazy_preload-gemfile: "ar_lazy_preload_master"
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                          - ruby-version: "2.5"
         | 
| 122 | 
            +
                            activerecord-gemfile: "ar_6_latest"
         | 
| 123 | 
            +
                            ar_lazy_preload-gemfile: "ar_lazy_preload_master"
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                          # AR 5 and ruby 3+
         | 
| 126 | 
            +
                          - ruby-version: "latest"
         | 
| 127 | 
            +
                            activerecord-gemfile: "ar_5_latest"
         | 
| 128 | 
            +
                            ar_lazy_preload-gemfile: "ar_lazy_preload_0.6.1"
         | 
| 129 | 
            +
                          - ruby-version: "latest"
         | 
| 130 | 
            +
                            activerecord-gemfile: "ar_5_latest"
         | 
| 131 | 
            +
                            ar_lazy_preload-gemfile: "ar_lazy_preload_master"
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                      name: ruby-<< matrix.ruby-version >>-<< matrix.activerecord-gemfile >>-<< matrix.ar_lazy_preload-gemfile >>
         | 
| 92 134 | 
             
                  - rubocop:
         | 
| 93 135 | 
             
                      requires:
         | 
| 94 136 | 
             
                        - checkout
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,5 +1,23 @@ | |
| 1 | 
            -
            ## [ | 
| 1 | 
            +
            ## [1.3.0] - 2022-02-22
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            - add support of named arguments with `argument <name>`
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            BREAKING CHANGES:
         | 
| 6 | 
            +
            - rename `n1_load` to `n1_optimized`
         | 
| 7 | 
            +
            - rework `def self.arguments_key` to `cache_key`
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ## [1.2.0] - 2022-01-14
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            - Introduce arguments support.
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            ## [1.1.0] - 2021-12-27
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            - Introduce `fulfill` method to abstract the storage.
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            ## [1.0.0] - 2021-12-26
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            - Various of great features.
         | 
| 2 20 |  | 
| 3 21 | 
             
            ## [0.1.0] - 2021-12-16
         | 
| 4 22 |  | 
| 5 | 
            -
            - Initial release
         | 
| 23 | 
            +
            - Initial release.
         | 
    
        data/Gemfile
    CHANGED
    
    | @@ -4,3 +4,12 @@ source "https://rubygems.org" | |
| 4 4 |  | 
| 5 5 | 
             
            # Specify your gem's dependencies in n1_loader.gemspec
         | 
| 6 6 | 
             
            gemspec
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            # Hack to make Github work with Circle CI job names with slashes
         | 
| 9 | 
            +
            gemfiles = []
         | 
| 10 | 
            +
            gemfiles << "activerecord-gemfiles/#{ENV["ACTIVERECORD_GEMFILE"]}.gemfile" if ENV["ACTIVERECORD_GEMFILE"]
         | 
| 11 | 
            +
            gemfiles << "ar_lazy_preload-gemfiles/#{ENV["AR_LAZY_PRELOAD_GEMFILE"]}.gemfile" if ENV["AR_LAZY_PRELOAD_GEMFILE"]
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            gemfiles.each do |path|
         | 
| 14 | 
            +
              eval(File.read(path)) # rubocop:disable Security/Eval
         | 
| 15 | 
            +
            end
         | 
    
        data/README.md
    CHANGED
    
    | @@ -9,11 +9,16 @@ We have a solution for you! | |
| 9 9 | 
             
            [N1Loader][8] is designed to solve the issue for good!
         | 
| 10 10 |  | 
| 11 11 | 
             
            It has many benefits:
         | 
| 12 | 
            -
            - it  | 
| 13 | 
            -
            - it  | 
| 12 | 
            +
            - it can be [isolated](#isolated-loaders)
         | 
| 13 | 
            +
            - it loads data [lazily](#lazy-loading)
         | 
| 14 | 
            +
            - it supports [shareable loaders](#shareable-loaders) between multiple classes
         | 
| 15 | 
            +
            - it supports [reloading](#reloading)
         | 
| 16 | 
            +
            - it supports optimized [single object loading](#optimized-single-case)
         | 
| 17 | 
            +
            - it supports [arguments](#arguments)
         | 
| 14 18 | 
             
            - it has an integration with [ActiveRecord][5] which makes it brilliant ([example](#activerecord))
         | 
| 15 19 | 
             
            - it has an integration with [ArLazyPreload][6] which makes it excellent ([example](#arlazypreload))
         | 
| 16 20 |  | 
| 21 | 
            +
            ... and even more features to come! Stay tuned!
         | 
| 17 22 |  | 
| 18 23 | 
             
            ## Installation
         | 
| 19 24 |  | 
| @@ -25,59 +30,221 @@ gem 'n1_loader' | |
| 25 30 |  | 
| 26 31 | 
             
            You can add integration with [ActiveRecord][5] by:
         | 
| 27 32 | 
             
            ```ruby
         | 
| 28 | 
            -
            require 'n1_loader/active_record'
         | 
| 33 | 
            +
            gem 'n1_loader', require: 'n1_loader/active_record'
         | 
| 29 34 | 
             
            ```
         | 
| 30 35 |  | 
| 31 36 | 
             
            You can add the integration with [ActiveRecord][5] and [ArLazyPreload][6] by:
         | 
| 32 37 | 
             
            ```ruby
         | 
| 33 | 
            -
            require 'n1_loader/ar_lazy_preload'
         | 
| 38 | 
            +
            gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
         | 
| 34 39 | 
             
            ```
         | 
| 35 40 |  | 
| 36 41 | 
             
            ## Usage
         | 
| 37 42 |  | 
| 38 43 | 
             
            ```ruby
         | 
| 39 | 
            -
            class  | 
| 44 | 
            +
            class User
         | 
| 40 45 | 
             
              include N1Loader::Loadable
         | 
| 41 46 |  | 
| 42 47 | 
             
              # with inline loader
         | 
| 43 | 
            -
               | 
| 44 | 
            -
                 | 
| 45 | 
            -
                 | 
| 48 | 
            +
              n1_optimized :orders_count do |users|
         | 
| 49 | 
            +
                orders_per_user = Order.where(user: users).group(:user_id).count
         | 
| 50 | 
            +
                
         | 
| 51 | 
            +
                users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 46 52 | 
             
              end
         | 
| 53 | 
            +
            end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            # For single object
         | 
| 56 | 
            +
            user = User.new
         | 
| 57 | 
            +
            user.orders_count 
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            # For multiple objects without N+1
         | 
| 60 | 
            +
            users = [User.new, User.new]
         | 
| 61 | 
            +
            N1Loader::Preloader.new(users).preload(:orders_count)
         | 
| 62 | 
            +
            users.map(&:orders_count)
         | 
| 63 | 
            +
            ```
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            ### Lazy loading
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            ```ruby
         | 
| 68 | 
            +
            class User
         | 
| 69 | 
            +
              include N1Loader::Loadable
         | 
| 70 | 
            +
             | 
| 71 | 
            +
              # with inline loader
         | 
| 72 | 
            +
              n1_optimized :orders_count do |users|
         | 
| 73 | 
            +
                orders_per_user = Order.where(user: users).group(:user_id).count
         | 
| 47 74 |  | 
| 48 | 
            -
             | 
| 49 | 
            -
               | 
| 75 | 
            +
                users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 76 | 
            +
              end
         | 
| 50 77 | 
             
            end
         | 
| 51 78 |  | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 79 | 
            +
            user = User.new # => nothing was done for loading
         | 
| 80 | 
            +
            user.orders_count # => first time loading
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            users = [User.new, User.new] # => nothing was done for loading
         | 
| 83 | 
            +
            N1Loader::Preloader.new([users]).preload(:orders_count) # => we only initialized loader but didn't perform it yet
         | 
| 84 | 
            +
            users.map(&:orders_count) # => loading has happen for the first time (without N+1)
         | 
| 85 | 
            +
            ```
         | 
| 86 | 
            +
             | 
| 87 | 
            +
             | 
| 88 | 
            +
            ### Shareable loaders
         | 
| 89 | 
            +
             | 
| 90 | 
            +
            ```ruby
         | 
| 91 | 
            +
            class OrdersCountLoader < N1Loader::Loader
         | 
| 92 | 
            +
              def perform(users)
         | 
| 93 | 
            +
                orders_per_user = Order.where(user: users).group(:user_id).count
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 96 | 
            +
              end
         | 
| 97 | 
            +
            end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
            class User
         | 
| 100 | 
            +
              include N1Loader::Loadable
         | 
| 101 | 
            +
             | 
| 102 | 
            +
              n1_optimized :orders_count, OrdersCountLoader
         | 
| 103 | 
            +
            end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
            class Customer
         | 
| 106 | 
            +
              include N1Loader::Loadable
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              n1_optimized :orders_count, OrdersCountLoader
         | 
| 109 | 
            +
            end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
            User.new.orders_count # => works
         | 
| 112 | 
            +
            Customer.new.orders_count  # => works
         | 
| 113 | 
            +
            ```
         | 
| 114 | 
            +
             | 
| 115 | 
            +
            ### Reloading
         | 
| 116 | 
            +
             | 
| 117 | 
            +
            ```ruby
         | 
| 118 | 
            +
            class User
         | 
| 119 | 
            +
              include N1Loader::Loadable
         | 
| 120 | 
            +
             | 
| 121 | 
            +
              # with inline loader
         | 
| 122 | 
            +
              n1_optimized :orders_count do |users|
         | 
| 123 | 
            +
                orders_per_user = Order.where(user: users).group(:user_id).count
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 126 | 
            +
              end
         | 
| 127 | 
            +
            end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
            user = User.new
         | 
| 130 | 
            +
            user.orders_count # => loader is executed first time and value was cached
         | 
| 131 | 
            +
            user.orders_count(reload: true) # => loader is executed again and a new value was cached
         | 
| 132 | 
            +
             | 
| 133 | 
            +
            users = [User.new, User.new]
         | 
| 134 | 
            +
            N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized but not yet executed
         | 
| 135 | 
            +
            users.map(&:orders_count) # => loader was executed first time without N+1 issue and values were cached
         | 
| 136 | 
            +
             | 
| 137 | 
            +
            N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized again but not yet executed
         | 
| 138 | 
            +
            users.map(&:orders_count) # => new loader was executed first time without N+1 issue and new values were cached
         | 
| 139 | 
            +
            ```
         | 
| 140 | 
            +
             | 
| 141 | 
            +
            ### Isolated loaders
         | 
| 142 | 
            +
             | 
| 143 | 
            +
            ```ruby
         | 
| 144 | 
            +
            class IsolatedLoader < N1Loader::Loader
         | 
| 55 145 | 
             
              def perform(elements)
         | 
| 56 | 
            -
                elements. | 
| 146 | 
            +
                elements.each { |element| fulfill(element, [element]) }
         | 
| 57 147 | 
             
              end
         | 
| 58 148 | 
             
            end
         | 
| 59 149 |  | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 150 | 
            +
            objects = [1, 2, 3, 4]
         | 
| 151 | 
            +
            loader = IsolatedLoader.new(objects)
         | 
| 152 | 
            +
            objects.each do |object|
         | 
| 153 | 
            +
              loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
         | 
| 154 | 
            +
            end
         | 
| 155 | 
            +
            ```
         | 
| 63 156 |  | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 157 | 
            +
            ### Optimized single case
         | 
| 158 | 
            +
             | 
| 159 | 
            +
            ```ruby
         | 
| 160 | 
            +
            class User
         | 
| 161 | 
            +
              include N1Loader::Loadable
         | 
| 162 | 
            +
             | 
| 163 | 
            +
              n1_optimized :orders_count do # no arguments passed to the block, so we can override both perform and single.
         | 
| 164 | 
            +
                def perform(users)
         | 
| 165 | 
            +
                  orders_per_user = Order.where(user: users).group(:user_id).count
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 168 | 
            +
                end
         | 
| 169 | 
            +
                
         | 
| 170 | 
            +
                # Optimized for single object loading
         | 
| 171 | 
            +
                def single(user)
         | 
| 172 | 
            +
                  user.orders.count
         | 
| 173 | 
            +
                end
         | 
| 174 | 
            +
              end
         | 
| 175 | 
            +
            end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
            user = User.new
         | 
| 178 | 
            +
            user.orders_count # single will be used here
         | 
| 179 | 
            +
             | 
| 180 | 
            +
            users = [User.new, User.new]
         | 
| 181 | 
            +
            N1Loader::Preloader.new(users).preload(:orders_count)
         | 
| 182 | 
            +
            users.map(&:orders_count) # perform will be used once without N+1
         | 
| 68 183 | 
             
            ```
         | 
| 69 184 |  | 
| 185 | 
            +
            ### Arguments
         | 
| 186 | 
            +
             | 
| 187 | 
            +
            ```ruby
         | 
| 188 | 
            +
            class User
         | 
| 189 | 
            +
              include N1Loader::Loadable
         | 
| 190 | 
            +
             | 
| 191 | 
            +
              n1_optimized :orders_count do |users, type|
         | 
| 192 | 
            +
                orders_per_user = Order.where(type: type, user: users).group(:user_id).count
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 195 | 
            +
              end
         | 
| 196 | 
            +
            end
         | 
| 197 | 
            +
             | 
| 198 | 
            +
            user = User.new
         | 
| 199 | 
            +
            user.orders_count(:gifts) # The loader will be performed first time for this argument
         | 
| 200 | 
            +
            user.orders_count(:sales) # The loader will be performed first time for this argument
         | 
| 201 | 
            +
            user.orders_count(:gifts) # The cached value will be used
         | 
| 202 | 
            +
             | 
| 203 | 
            +
            users = [User.new, User.new]
         | 
| 204 | 
            +
            N1Loader::Preloader.new(users).preload(:orders_count)
         | 
| 205 | 
            +
            users.map { |user| user.orders_count(:gifts) } # No N+1 here
         | 
| 206 | 
            +
            ```
         | 
| 207 | 
            +
             | 
| 208 | 
            +
            _Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases, 
         | 
| 209 | 
            +
            you may want to override it, for example:
         | 
| 210 | 
            +
             | 
| 211 | 
            +
            ```ruby
         | 
| 212 | 
            +
            class User
         | 
| 213 | 
            +
              include N1Loader::Loadable
         | 
| 214 | 
            +
             | 
| 215 | 
            +
              n1_optimized :orders_count do
         | 
| 216 | 
            +
                argument :sale 
         | 
| 217 | 
            +
                
         | 
| 218 | 
            +
                cache_key { sale.id }
         | 
| 219 | 
            +
                
         | 
| 220 | 
            +
                def perform(users, sale)
         | 
| 221 | 
            +
                  orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count
         | 
| 222 | 
            +
                  
         | 
| 223 | 
            +
                  users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 224 | 
            +
                end
         | 
| 225 | 
            +
              end
         | 
| 226 | 
            +
            end
         | 
| 227 | 
            +
             | 
| 228 | 
            +
            user = User.new
         | 
| 229 | 
            +
            user.orders_count(Sale.first) # perform will be executed and value will be cached
         | 
| 230 | 
            +
            user.orders_count(Sale.first) # the cached value will be returned
         | 
| 231 | 
            +
            ```
         | 
| 232 | 
            +
             | 
| 233 | 
            +
             | 
| 234 | 
            +
            ## Integrations
         | 
| 235 | 
            +
             | 
| 70 236 | 
             
            ### [ActiveRecord][5]
         | 
| 71 237 |  | 
| 238 | 
            +
            _Note_: Rails 7 support is coming soon! Stay tuned!
         | 
| 239 | 
            +
             | 
| 72 240 | 
             
            ```ruby
         | 
| 73 241 | 
             
            class User < ActiveRecord::Base
         | 
| 74 242 | 
             
              include N1Loader::Loadable
         | 
| 75 | 
            -
             | 
| 76 | 
            -
               | 
| 77 | 
            -
                 | 
| 78 | 
            -
             | 
| 79 | 
            -
                 | 
| 80 | 
            -
                hash.transform_keys! { |key| users.find { |user| user.id == key } }
         | 
| 243 | 
            +
             | 
| 244 | 
            +
              n1_optimized :orders_count do |users|
         | 
| 245 | 
            +
                orders_per_user = Order.where(user: users).group(:user_id).count
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 81 248 | 
             
              end
         | 
| 82 249 | 
             
            end
         | 
| 83 250 |  | 
| @@ -101,12 +268,11 @@ users.map(&:orders_count) | |
| 101 268 | 
             
            ```ruby
         | 
| 102 269 | 
             
            class User < ActiveRecord::Base
         | 
| 103 270 | 
             
              include N1Loader::Loadable
         | 
| 104 | 
            -
             | 
| 105 | 
            -
               | 
| 106 | 
            -
                 | 
| 107 | 
            -
             | 
| 108 | 
            -
                 | 
| 109 | 
            -
                hash.transform_keys! { |key| users.find { |user| user.id == key } }
         | 
| 271 | 
            +
             | 
| 272 | 
            +
              n1_optimized :orders_count do |users|
         | 
| 273 | 
            +
                orders_per_user = Order.where(user: users).group(:user_id).count
         | 
| 274 | 
            +
             | 
| 275 | 
            +
                users.each { |user| fulfill(user, orders_per_user[user.id]) }
         | 
| 110 276 | 
             
              end
         | 
| 111 277 | 
             
            end
         | 
| 112 278 |  | 
| @@ -123,12 +289,6 @@ ArLazyPreload.config.auto_preload = true | |
| 123 289 | 
             
            User.all.map(:orders_count)
         | 
| 124 290 | 
             
            ```
         | 
| 125 291 |  | 
| 126 | 
            -
            ## Development
         | 
| 127 | 
            -
             | 
| 128 | 
            -
            After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
         | 
| 129 | 
            -
             | 
| 130 | 
            -
            To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
         | 
| 131 | 
            -
             | 
| 132 292 | 
             
            ## Contributing
         | 
| 133 293 |  | 
| 134 294 | 
             
            Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader. 
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module N1Loader
         | 
| 4 | 
            +
              module ActiveRecord
         | 
| 5 | 
            +
                module Associations
         | 
| 6 | 
            +
                  module Preloader # :nodoc:
         | 
| 7 | 
            +
                    N1LoaderReflection = Struct.new(:key, :loader) do
         | 
| 8 | 
            +
                      def options
         | 
| 9 | 
            +
                        {}
         | 
| 10 | 
            +
                      end
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def preloaders_for_one(association, records, scope)
         | 
| 14 | 
            +
                      grouped_records(association, records).flat_map do |reflection, klasses|
         | 
| 15 | 
            +
                        next N1Loader::Preloader.new(records).preload(reflection.key) if reflection.is_a?(N1LoaderReflection)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                        klasses.map do |rhs_klass, rs|
         | 
| 18 | 
            +
                          loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
         | 
| 19 | 
            +
                          loader.run self
         | 
| 20 | 
            +
                          loader
         | 
| 21 | 
            +
                        end
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def grouped_records(association, records)
         | 
| 26 | 
            +
                      n1_load_records, records = records.partition do |record|
         | 
| 27 | 
            +
                        record.class.respond_to?(:n1_loader_defined?) && record.class.n1_loader_defined?(association)
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      hash = n1_load_records.group_by do |record|
         | 
| 31 | 
            +
                        N1LoaderReflection.new(association, record.class.n1_loader(association))
         | 
| 32 | 
            +
                      end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      hash.merge(super)
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
            end
         | 
| 
            File without changes
         | 
| @@ -0,0 +1,9 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            N1Loader::LoaderCollection.define_method :preloaded_records do
         | 
| 4 | 
            +
              unless loader_class.instance_method(:perform).arity == 1
         | 
| 5 | 
            +
                raise N1Loader::ActiveRecord::InvalidPreloading, "Cannot preload loader with arguments"
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              with.preloaded_records
         | 
| 9 | 
            +
            end
         | 
| @@ -1,11 +1,28 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            # Load core library
         | 
| 3 4 | 
             
            require_relative "../n1_loader"
         | 
| 4 5 |  | 
| 6 | 
            +
            # Load integration dependency
         | 
| 5 7 | 
             
            require "active_record"
         | 
| 6 8 |  | 
| 7 | 
            -
             | 
| 9 | 
            +
            module N1Loader
         | 
| 10 | 
            +
              module ActiveRecord
         | 
| 11 | 
            +
                class InvalidPreloading < N1Loader::Error; end
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| 8 14 |  | 
| 15 | 
            +
            # Library integration
         | 
| 9 16 | 
             
            ActiveSupport.on_load(:active_record) do
         | 
| 17 | 
            +
              require_relative "active_record/loader"
         | 
| 18 | 
            +
              require_relative "active_record/loader_collection"
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              case ActiveRecord::VERSION::MAJOR
         | 
| 21 | 
            +
              when 6
         | 
| 22 | 
            +
                require_relative "active_record/associations_preloader_v6"
         | 
| 23 | 
            +
              else
         | 
| 24 | 
            +
                require_relative "active_record/associations_preloader_v5"
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 10 27 | 
             
              ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
         | 
| 11 28 | 
             
            end
         | 
| @@ -4,9 +4,16 @@ module N1Loader | |
| 4 4 | 
             
              module ArLazyPreload
         | 
| 5 5 | 
             
                # Context builder for N1Loader
         | 
| 6 6 | 
             
                class AssociatedContextBuilder < ::ArLazyPreload::AssociatedContextBuilder
         | 
| 7 | 
            +
                  attr_reader :records
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def initialize(parent_context:, association_name:, records:)
         | 
| 10 | 
            +
                    super(parent_context: parent_context, association_name: association_name)
         | 
| 11 | 
            +
                    @records = records
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 7 14 | 
             
                  def perform
         | 
| 8 15 | 
             
                    ::ArLazyPreload::Context.register(
         | 
| 9 | 
            -
                      records:  | 
| 16 | 
            +
                      records: records.flatten(1).select { |record| record.respond_to?(:lazy_preload_context=) },
         | 
| 10 17 | 
             
                      association_tree: child_association_tree,
         | 
| 11 18 | 
             
                      auto_preload: parent_context.auto_preload?
         | 
| 12 19 | 
             
                    )
         | 
| @@ -6,7 +6,7 @@ module N1Loader | |
| 6 6 | 
             
                class ContextAdapter
         | 
| 7 7 | 
             
                  attr_reader :context
         | 
| 8 8 |  | 
| 9 | 
            -
                   | 
| 9 | 
            +
                  delegate_missing_to :context
         | 
| 10 10 |  | 
| 11 11 | 
             
                  def initialize(context)
         | 
| 12 12 | 
             
                    @context = context
         | 
| @@ -19,12 +19,15 @@ module N1Loader | |
| 19 19 | 
             
                  end
         | 
| 20 20 |  | 
| 21 21 | 
             
                  def perform_preloading(association_name)
         | 
| 22 | 
            -
                     | 
| 22 | 
            +
                    context_setup = lambda { |records|
         | 
| 23 | 
            +
                      AssociatedContextBuilder.prepare(
         | 
| 24 | 
            +
                        parent_context: self,
         | 
| 25 | 
            +
                        association_name: association_name,
         | 
| 26 | 
            +
                        records: records
         | 
| 27 | 
            +
                      )
         | 
| 28 | 
            +
                    }
         | 
| 23 29 |  | 
| 24 | 
            -
                     | 
| 25 | 
            -
                      parent_context: self,
         | 
| 26 | 
            -
                      association_name: association_name
         | 
| 27 | 
            -
                    )
         | 
| 30 | 
            +
                    N1Loader::Preloader.new(records, context_setup).preload(association_name)
         | 
| 28 31 | 
             
                  end
         | 
| 29 32 | 
             
                end
         | 
| 30 33 | 
             
              end
         | 
| @@ -4,18 +4,18 @@ module N1Loader | |
| 4 4 | 
             
              module ArLazyPreload
         | 
| 5 5 | 
             
                module Loadable
         | 
| 6 6 | 
             
                  module ClassMethods # :nodoc:
         | 
| 7 | 
            -
                    def  | 
| 7 | 
            +
                    def n1_optimized(name, loader = nil, &block)
         | 
| 8 8 | 
             
                      name, loader_name, loader_variable_name = super
         | 
| 9 9 |  | 
| 10 10 | 
             
                      define_method(loader_name) do
         | 
| 11 11 | 
             
                        loader = instance_variable_get(loader_variable_name)
         | 
| 12 | 
            -
             | 
| 13 12 | 
             
                        return loader if loader
         | 
| 13 | 
            +
             | 
| 14 14 | 
             
                        if respond_to?(:lazy_preload_context) && ContextAdapter.new(lazy_preload_context).try_preload_lazily(name)
         | 
| 15 15 | 
             
                          return instance_variable_get(loader_variable_name)
         | 
| 16 16 | 
             
                        end
         | 
| 17 17 |  | 
| 18 | 
            -
                         | 
| 18 | 
            +
                        send("#{loader_name}_reload")
         | 
| 19 19 | 
             
                      end
         | 
| 20 20 | 
             
                    end
         | 
| 21 21 | 
             
                  end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module N1Loader
         | 
| 4 | 
            +
              module ArLazyPreload
         | 
| 5 | 
            +
                # A patch to {N1Loader::LoaderCollection} to setup lazy context lazily.
         | 
| 6 | 
            +
                module LoaderCollectionPatch
         | 
| 7 | 
            +
                  attr_accessor :context_setup
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def with(*args)
         | 
| 10 | 
            +
                    result = super
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    result.context_setup = context_setup if context_setup && result.context_setup.nil?
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    result
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module N1Loader
         | 
| 4 | 
            +
              module ArLazyPreload
         | 
| 5 | 
            +
                # A patch to {N1Loader::Loader} to setup lazy context lazily.
         | 
| 6 | 
            +
                module LoaderPatch
         | 
| 7 | 
            +
                  attr_accessor :context_setup
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def loaded
         | 
| 10 | 
            +
                    return @loaded if @loaded
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    super
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    context_setup&.call(preloaded_records)
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    @loaded
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module N1Loader
         | 
| 4 | 
            +
              module ArLazyPreload
         | 
| 5 | 
            +
                # A patch to {N1Loader::Preloader} setup lazy context lazily.
         | 
| 6 | 
            +
                module PreloaderPatch
         | 
| 7 | 
            +
                  def initialize(elements, context_setup = nil)
         | 
| 8 | 
            +
                    super(elements)
         | 
| 9 | 
            +
                    @context_setup = context_setup
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def preload(*keys)
         | 
| 13 | 
            +
                    super.each do |loader_collection|
         | 
| 14 | 
            +
                      loader_collection.context_setup = context_setup
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  private
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  attr_reader :context_setup
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -1,12 +1,21 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            # Load core library
         | 
| 3 4 | 
             
            require_relative "active_record"
         | 
| 4 5 |  | 
| 6 | 
            +
            # Load integration dependency
         | 
| 5 7 | 
             
            require "rails"
         | 
| 6 8 | 
             
            require "ar_lazy_preload"
         | 
| 7 9 |  | 
| 10 | 
            +
            # Library integration
         | 
| 8 11 | 
             
            require_relative "ar_lazy_preload/loadable"
         | 
| 9 12 | 
             
            require_relative "ar_lazy_preload/context_adapter"
         | 
| 10 13 | 
             
            require_relative "ar_lazy_preload/associated_context_builder"
         | 
| 14 | 
            +
            require_relative "ar_lazy_preload/loader_collection_patch"
         | 
| 15 | 
            +
            require_relative "ar_lazy_preload/preloader_patch"
         | 
| 16 | 
            +
            require_relative "ar_lazy_preload/loader_patch"
         | 
| 11 17 |  | 
| 12 18 | 
             
            N1Loader::Loadable::ClassMethods.prepend(N1Loader::ArLazyPreload::Loadable::ClassMethods)
         | 
| 19 | 
            +
            N1Loader::Preloader.prepend(N1Loader::ArLazyPreload::PreloaderPatch)
         | 
| 20 | 
            +
            N1Loader::Loader.prepend(N1Loader::ArLazyPreload::LoaderPatch)
         | 
| 21 | 
            +
            N1Loader::LoaderCollection.prepend(N1Loader::ArLazyPreload::LoaderCollectionPatch)
         | 
| @@ -7,9 +7,9 @@ module N1Loader | |
| 7 7 | 
             
              #     include N1Loader::Loadable
         | 
| 8 8 | 
             
              #
         | 
| 9 9 | 
             
              #     # with inline loader
         | 
| 10 | 
            -
              #     n1_loader :something do | 
| 11 | 
            -
              #       elements | 
| 12 | 
            -
              #          | 
| 10 | 
            +
              #     n1_loader :something do
         | 
| 11 | 
            +
              #       def perform(elements)
         | 
| 12 | 
            +
              #         elements.each { |element| fulfill(element,, element.calculate_something) }
         | 
| 13 13 | 
             
              #       end
         | 
| 14 14 | 
             
              #     end
         | 
| 15 15 | 
             
              #
         | 
| @@ -20,9 +20,7 @@ module N1Loader | |
| 20 20 | 
             
              #   # custom loader
         | 
| 21 21 | 
             
              #   class MyLoader < N1Loader::Loader
         | 
| 22 22 | 
             
              #     def perform(elements)
         | 
| 23 | 
            -
              #       elements. | 
| 24 | 
            -
              #         hash[element] = element.calculate_something
         | 
| 25 | 
            -
              #       end
         | 
| 23 | 
            +
              #       elements.each { |element| fulfill(element,, element.calculate_something) }
         | 
| 26 24 | 
             
              #     end
         | 
| 27 25 | 
             
              #   end
         | 
| 28 26 | 
             
              module Loadable
         | 
| @@ -30,8 +28,8 @@ module N1Loader | |
| 30 28 | 
             
                  send("#{name}_loader")
         | 
| 31 29 | 
             
                end
         | 
| 32 30 |  | 
| 33 | 
            -
                def n1_loader_set(name,  | 
| 34 | 
            -
                  send("#{name}_loader=",  | 
| 31 | 
            +
                def n1_loader_set(name, loader_collection)
         | 
| 32 | 
            +
                  send("#{name}_loader=", loader_collection)
         | 
| 35 33 | 
             
                end
         | 
| 36 34 |  | 
| 37 35 | 
             
                def self.included(base)
         | 
| @@ -47,11 +45,14 @@ module N1Loader | |
| 47 45 | 
             
                    respond_to?("#{name}_loader")
         | 
| 48 46 | 
             
                  end
         | 
| 49 47 |  | 
| 50 | 
            -
                  def  | 
| 48 | 
            +
                  def n1_optimized(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
         | 
| 51 49 | 
             
                    loader ||= Class.new(N1Loader::Loader) do
         | 
| 52 | 
            -
                       | 
| 50 | 
            +
                      if block&.arity&.positive?
         | 
| 51 | 
            +
                        define_method(:perform, &block)
         | 
| 52 | 
            +
                      else
         | 
| 53 | 
            +
                        class_eval(&block)
         | 
| 54 | 
            +
                      end
         | 
| 53 55 | 
             
                    end
         | 
| 54 | 
            -
             | 
| 55 56 | 
             
                    loader_name = "#{name}_loader"
         | 
| 56 57 | 
             
                    loader_variable_name = "@#{loader_name}"
         | 
| 57 58 |  | 
| @@ -59,17 +60,23 @@ module N1Loader | |
| 59 60 | 
             
                      loader
         | 
| 60 61 | 
             
                    end
         | 
| 61 62 |  | 
| 62 | 
            -
                    define_method("#{loader_name} | 
| 63 | 
            -
                      instance_variable_set(loader_variable_name, | 
| 63 | 
            +
                    define_method("#{loader_name}_reload") do
         | 
| 64 | 
            +
                      instance_variable_set(loader_variable_name,
         | 
| 65 | 
            +
                                            N1Loader::LoaderCollection.new(self.class.send(loader_name), [self]))
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    define_method("#{loader_name}=") do |loader_collection_instance|
         | 
| 69 | 
            +
                      instance_variable_set(loader_variable_name, loader_collection_instance)
         | 
| 64 70 | 
             
                    end
         | 
| 65 71 |  | 
| 66 72 | 
             
                    define_method(loader_name) do
         | 
| 67 | 
            -
                      instance_variable_get(loader_variable_name) ||
         | 
| 68 | 
            -
                        instance_variable_set(loader_variable_name, self.class.send(loader_name).new([self]))
         | 
| 73 | 
            +
                      instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
         | 
| 69 74 | 
             
                    end
         | 
| 70 75 |  | 
| 71 | 
            -
                    define_method(name) do
         | 
| 72 | 
            -
                      send(loader_name) | 
| 76 | 
            +
                    define_method(name) do |*args, reload: false|
         | 
| 77 | 
            +
                      send("#{loader_name}_reload") if reload
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                      send(loader_name).with(*args).for(self)
         | 
| 73 80 | 
             
                    end
         | 
| 74 81 |  | 
| 75 82 | 
             
                    [name, loader_name, loader_variable_name]
         | 
| @@ -6,30 +6,80 @@ module N1Loader | |
| 6 6 | 
             
              # Subclasses must define +perform+ method that accepts single argument
         | 
| 7 7 | 
             
              # and returns hash where key is the element and value is what we want to load.
         | 
| 8 8 | 
             
              class Loader
         | 
| 9 | 
            -
                 | 
| 9 | 
            +
                class << self
         | 
| 10 | 
            +
                  attr_reader :arguments
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  # Defines an argument that can be accessed within the loader.
         | 
| 13 | 
            +
                  #
         | 
| 14 | 
            +
                  # First defined argument will have the value of first passed argument,
         | 
| 15 | 
            +
                  # meaning the order is important.
         | 
| 16 | 
            +
                  def argument(name)
         | 
| 17 | 
            +
                    @arguments ||= []
         | 
| 18 | 
            +
                    index = @arguments.size
         | 
| 19 | 
            +
                    define_method(name) { args[index] }
         | 
| 20 | 
            +
                    @arguments << name
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  # Defines a custom cache key that is calculated for passed arguments.
         | 
| 24 | 
            +
                  def cache_key(&block)
         | 
| 25 | 
            +
                    define_method(:cache_key) do
         | 
| 26 | 
            +
                      check_arguments!
         | 
| 27 | 
            +
                      instance_exec(&block)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def initialize(elements, *args)
         | 
| 10 33 | 
             
                  @elements = elements
         | 
| 34 | 
            +
                  @args = args
         | 
| 11 35 | 
             
                end
         | 
| 12 36 |  | 
| 13 | 
            -
                def  | 
| 14 | 
            -
                   | 
| 37 | 
            +
                def for(element)
         | 
| 38 | 
            +
                  if loaded.empty? && elements.any?
         | 
| 39 | 
            +
                    raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                  raise NotLoaded, "The data was not preloaded for the given element" unless loaded.key?(element)
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  loaded[element]
         | 
| 15 44 | 
             
                end
         | 
| 16 45 |  | 
| 17 | 
            -
                def  | 
| 18 | 
            -
                   | 
| 46 | 
            +
                def cache_key
         | 
| 47 | 
            +
                  args.map(&:object_id)
         | 
| 19 48 | 
             
                end
         | 
| 20 49 |  | 
| 21 | 
            -
                 | 
| 22 | 
            -
             | 
| 50 | 
            +
                private
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                attr_reader :elements, :args
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def check_arguments!
         | 
| 55 | 
            +
                  return unless (required = self.class.arguments)
         | 
| 56 | 
            +
                  return if required.size == args.size
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  raise MissingArgument, "Loader defined #{required.size} arguments but #{args.size} were given"
         | 
| 23 59 | 
             
                end
         | 
| 24 60 |  | 
| 25 | 
            -
                def  | 
| 26 | 
            -
                  raise  | 
| 61 | 
            +
                def perform(_elements)
         | 
| 62 | 
            +
                  raise NotImplemented, "Subclasses have to implement the method"
         | 
| 63 | 
            +
                end
         | 
| 27 64 |  | 
| 28 | 
            -
             | 
| 65 | 
            +
                def fulfill(element, value)
         | 
| 66 | 
            +
                  @loaded[element] = value
         | 
| 29 67 | 
             
                end
         | 
| 30 68 |  | 
| 31 | 
            -
                 | 
| 69 | 
            +
                def loaded # rubocop:disable Metrics/AbcSize
         | 
| 70 | 
            +
                  return @loaded if @loaded
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  check_arguments!
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  @loaded = {}.compare_by_identity
         | 
| 32 75 |  | 
| 33 | 
            -
             | 
| 76 | 
            +
                  if elements.size == 1 && respond_to?(:single)
         | 
| 77 | 
            +
                    fulfill(elements.first, single(elements.first, *args))
         | 
| 78 | 
            +
                  elsif elements.any?
         | 
| 79 | 
            +
                    perform(elements, *args)
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  @loaded
         | 
| 83 | 
            +
                end
         | 
| 34 84 | 
             
              end
         | 
| 35 85 | 
             
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module N1Loader
         | 
| 4 | 
            +
              # The class is used for storing collections of loaders for elements per set of arguments.
         | 
| 5 | 
            +
              class LoaderCollection
         | 
| 6 | 
            +
                attr_reader :loader_class, :elements
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(loader_class, elements)
         | 
| 9 | 
            +
                  @loader_class = loader_class
         | 
| 10 | 
            +
                  @elements = elements
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def with(*args)
         | 
| 14 | 
            +
                  loader = loader_class.new(elements, *args)
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  loaders[loader.cache_key] ||= loader
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                private
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def loaders
         | 
| 22 | 
            +
                  @loaders ||= {}
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| @@ -19,9 +19,9 @@ module N1Loader | |
| 19 19 | 
             
                    elements
         | 
| 20 20 | 
             
                      .group_by { |element| element.class.n1_loader(key) }
         | 
| 21 21 | 
             
                      .map do |loader_class, grouped_elements|
         | 
| 22 | 
            -
                         | 
| 23 | 
            -
                        grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key,  | 
| 24 | 
            -
                         | 
| 22 | 
            +
                        loader_collection = N1Loader::LoaderCollection.new(loader_class, grouped_elements)
         | 
| 23 | 
            +
                        grouped_elements.each { |grouped_element| grouped_element.n1_loader_set(key, loader_collection) }
         | 
| 24 | 
            +
                        loader_collection
         | 
| 25 25 | 
             
                      end
         | 
| 26 26 | 
             
                  end
         | 
| 27 27 | 
             
                end
         | 
    
        data/lib/n1_loader/version.rb
    CHANGED
    
    
    
        data/lib/n1_loader.rb
    CHANGED
    
    | @@ -3,6 +3,7 @@ | |
| 3 3 | 
             
            require_relative "n1_loader/version"
         | 
| 4 4 |  | 
| 5 5 | 
             
            require_relative "n1_loader/core/loader"
         | 
| 6 | 
            +
            require_relative "n1_loader/core/loader_collection"
         | 
| 6 7 | 
             
            require_relative "n1_loader/core/loadable"
         | 
| 7 8 | 
             
            require_relative "n1_loader/core/preloader"
         | 
| 8 9 |  | 
| @@ -10,4 +11,6 @@ module N1Loader # :nodoc: | |
| 10 11 | 
             
              class Error < StandardError; end
         | 
| 11 12 | 
             
              class NotImplemented < Error; end
         | 
| 12 13 | 
             
              class NotLoaded < Error; end
         | 
| 14 | 
            +
              class NotFilled < Error; end
         | 
| 15 | 
            +
              class MissingArgument < Error; end
         | 
| 13 16 | 
             
            end
         | 
    
        data/n1_loader.gemspec
    CHANGED
    
    | @@ -8,7 +8,7 @@ Gem::Specification.new do |spec| | |
| 8 8 | 
             
              spec.authors       = ["Evgeniy Demin"]
         | 
| 9 9 | 
             
              spec.email         = ["lawliet.djez@gmail.com"]
         | 
| 10 10 |  | 
| 11 | 
            -
              spec.summary       = " | 
| 11 | 
            +
              spec.summary       = "Loader to solve N+1 issue for good."
         | 
| 12 12 | 
             
              spec.homepage      = "https://github.com/djezzzl/n1_loader"
         | 
| 13 13 | 
             
              spec.license       = "MIT"
         | 
| 14 14 | 
             
              spec.required_ruby_version = ">= 2.5.0"
         | 
| @@ -22,10 +22,10 @@ Gem::Specification.new do |spec| | |
| 22 22 | 
             
              end
         | 
| 23 23 | 
             
              spec.require_paths = ["lib"]
         | 
| 24 24 |  | 
| 25 | 
            -
              spec.add_development_dependency "activerecord", " | 
| 26 | 
            -
              spec.add_development_dependency "ar_lazy_preload", " | 
| 25 | 
            +
              spec.add_development_dependency "activerecord", ">= 5"
         | 
| 26 | 
            +
              spec.add_development_dependency "ar_lazy_preload", ">= 0.6"
         | 
| 27 27 | 
             
              spec.add_development_dependency "db-query-matchers", "~> 0.10"
         | 
| 28 | 
            -
              spec.add_development_dependency "rails", " | 
| 28 | 
            +
              spec.add_development_dependency "rails", ">= 5"
         | 
| 29 29 | 
             
              spec.add_development_dependency "rspec", "~> 3.0"
         | 
| 30 30 | 
             
              spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
         | 
| 31 31 | 
             
              spec.add_development_dependency "rubocop", "~> 1.7"
         | 
    
        metadata
    CHANGED
    
    | @@ -1,43 +1,43 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: n1_loader
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version:  | 
| 4 | 
            +
              version: 1.3.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Evgeniy Demin
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2022-02-22 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activerecord
         | 
| 15 15 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 16 | 
             
                requirements:
         | 
| 17 | 
            -
                - - " | 
| 17 | 
            +
                - - ">="
         | 
| 18 18 | 
             
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            -
                    version: ' | 
| 19 | 
            +
                    version: '5'
         | 
| 20 20 | 
             
              type: :development
         | 
| 21 21 | 
             
              prerelease: false
         | 
| 22 22 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 23 | 
             
                requirements:
         | 
| 24 | 
            -
                - - " | 
| 24 | 
            +
                - - ">="
         | 
| 25 25 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            -
                    version: ' | 
| 26 | 
            +
                    version: '5'
         | 
| 27 27 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 28 28 | 
             
              name: ar_lazy_preload
         | 
| 29 29 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 30 | 
             
                requirements:
         | 
| 31 | 
            -
                - - " | 
| 31 | 
            +
                - - ">="
         | 
| 32 32 | 
             
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            -
                    version: '0. | 
| 33 | 
            +
                    version: '0.6'
         | 
| 34 34 | 
             
              type: :development
         | 
| 35 35 | 
             
              prerelease: false
         | 
| 36 36 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 37 | 
             
                requirements:
         | 
| 38 | 
            -
                - - " | 
| 38 | 
            +
                - - ">="
         | 
| 39 39 | 
             
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            -
                    version: '0. | 
| 40 | 
            +
                    version: '0.6'
         | 
| 41 41 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 42 42 | 
             
              name: db-query-matchers
         | 
| 43 43 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -56,16 +56,16 @@ dependencies: | |
| 56 56 | 
             
              name: rails
         | 
| 57 57 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 58 | 
             
                requirements:
         | 
| 59 | 
            -
                - - " | 
| 59 | 
            +
                - - ">="
         | 
| 60 60 | 
             
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            -
                    version: ' | 
| 61 | 
            +
                    version: '5'
         | 
| 62 62 | 
             
              type: :development
         | 
| 63 63 | 
             
              prerelease: false
         | 
| 64 64 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 65 | 
             
                requirements:
         | 
| 66 | 
            -
                - - " | 
| 66 | 
            +
                - - ">="
         | 
| 67 67 | 
             
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            -
                    version: ' | 
| 68 | 
            +
                    version: '5'
         | 
| 69 69 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 70 70 | 
             
              name: rspec
         | 
| 71 71 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -139,18 +139,28 @@ files: | |
| 139 139 | 
             
            - LICENSE.txt
         | 
| 140 140 | 
             
            - README.md
         | 
| 141 141 | 
             
            - Rakefile
         | 
| 142 | 
            +
            - activerecord-gemfiles/ar_5_latest.gemfile
         | 
| 143 | 
            +
            - activerecord-gemfiles/ar_6_latest.gemfile
         | 
| 144 | 
            +
            - ar_lazy_preload-gemfiles/ar_lazy_preload_0.6.1.gemfile
         | 
| 145 | 
            +
            - ar_lazy_preload-gemfiles/ar_lazy_preload_master.gemfile
         | 
| 142 146 | 
             
            - bin/console
         | 
| 143 147 | 
             
            - bin/setup
         | 
| 144 | 
            -
            - gemfiles/ar_6_latest.gemfile
         | 
| 145 148 | 
             
            - lib/n1_loader.rb
         | 
| 146 149 | 
             
            - lib/n1_loader/active_record.rb
         | 
| 147 | 
            -
            - lib/n1_loader/active_record/ | 
| 150 | 
            +
            - lib/n1_loader/active_record/associations_preloader_v5.rb
         | 
| 151 | 
            +
            - lib/n1_loader/active_record/associations_preloader_v6.rb
         | 
| 152 | 
            +
            - lib/n1_loader/active_record/loader.rb
         | 
| 153 | 
            +
            - lib/n1_loader/active_record/loader_collection.rb
         | 
| 148 154 | 
             
            - lib/n1_loader/ar_lazy_preload.rb
         | 
| 149 155 | 
             
            - lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
         | 
| 150 156 | 
             
            - lib/n1_loader/ar_lazy_preload/context_adapter.rb
         | 
| 151 157 | 
             
            - lib/n1_loader/ar_lazy_preload/loadable.rb
         | 
| 158 | 
            +
            - lib/n1_loader/ar_lazy_preload/loader_collection_patch.rb
         | 
| 159 | 
            +
            - lib/n1_loader/ar_lazy_preload/loader_patch.rb
         | 
| 160 | 
            +
            - lib/n1_loader/ar_lazy_preload/preloader_patch.rb
         | 
| 152 161 | 
             
            - lib/n1_loader/core/loadable.rb
         | 
| 153 162 | 
             
            - lib/n1_loader/core/loader.rb
         | 
| 163 | 
            +
            - lib/n1_loader/core/loader_collection.rb
         | 
| 154 164 | 
             
            - lib/n1_loader/core/preloader.rb
         | 
| 155 165 | 
             
            - lib/n1_loader/version.rb
         | 
| 156 166 | 
             
            - n1_loader.gemspec
         | 
| @@ -179,5 +189,5 @@ requirements: [] | |
| 179 189 | 
             
            rubygems_version: 3.2.22
         | 
| 180 190 | 
             
            signing_key: 
         | 
| 181 191 | 
             
            specification_version: 4
         | 
| 182 | 
            -
            summary:  | 
| 192 | 
            +
            summary: Loader to solve N+1 issue for good.
         | 
| 183 193 | 
             
            test_files: []
         |