README.md in n1_loader-1.4.3 vs README.md in n1_loader-1.4.4

- old
+ new

@@ -1,321 +1,150 @@ # N1Loader [![CircleCI][1]][2] [![Gem Version][3]][4] -Are you tired of fixing [N+1 issues][7]? Does it feel unnatural to you to fix it case by case in places where you need the data? -We have a solution for you! +N1Loader is designed to provide a way for avoiding [N+1 issues][7] of any kind. +For example, it can help with resolving N+1 for: +- database querying (most common case) +- 3rd party service calls +- complex calculations +- and many more -[N1Loader][8] is designed to solve the issue for good! +> [Toptal](https://www.toptal.com#snag-only-shrewd-web-development-experts) is hiring! [Join](https://www.toptal.com#snag-only-shrewd-web-development-experts) as Freelancer or [write me](mailto:lawliet.djez@gmail.com) if you want to join Core team. -It has many benefits: -- it can be [isolated](#isolated-loaders) -- it loads data [lazily](#lazy-loading) -- it supports [shareable loaders](#shareable-loaders) between multiple classes -- it supports [reloading](#reloading) -- it supports optimized [single object loading](#optimized-single-case) -- it supports [arguments](#arguments) -- it has an integration with [ActiveRecord][5] which makes it brilliant ([example](#activerecord)) -- it has an integration with [ArLazyPreload][6] which makes it excellent ([example](#arlazypreload)) +## Killer feature for GraphQL API -... and even more features to come! Stay tuned! +N1Loader in combination with [ArLazyPreload][6] is a killer feature for your GraphQL API. +Give it a try now and see incredible results instantly! Check out the [example](examples/graphql.rb) and start benefiting from it in your projects! -## Installation - -Add this line to your application's Gemfile: - ```ruby -gem 'n1_loader' +gem 'n1_loader', require: 'n1_loader/ar_lazy_preload' ``` -You can add integration with [ActiveRecord][5] by: -```ruby -gem 'n1_loader', require: 'n1_loader/active_record' +## Enhance [ActiveRecord][5] -# You also may be interested in injecting it to models -class ActiveRecord::Base - include N1Loader::Loadable - - def reload(*) - n1_clear_cache - super - end -end -``` +Are you working with well-known Rails application? Try it out how well N1Loader fulfills missing gaps! -You can add the integration with [ActiveRecord][5] and [ArLazyPreload][6] by: ```ruby -gem 'n1_loader', require: 'n1_loader/ar_lazy_preload' - -# You also may be interested in injecting it to models -class ActiveRecord::Base - include N1Loader::Loadable - - def reload(*) - n1_clear_cache - super - end -end +gem 'n1_loader', require: 'n1_loader/active_record' ``` -## Usage +Are you ready to forget about N+1 once and for all? Install [ArLazyPreload][6] and see dreams come true! ```ruby -class User - include N1Loader::Loadable - - # with inline loader - n1_optimized :orders_count do |users| - orders_per_user = Order.where(user: users).group(:user_id).count - - users.each { |user| fulfill(user, orders_per_user[user.id]) } - end -end - -# For single object -user = User.new -user.orders_count - -# For multiple objects without N+1 -users = [User.new, User.new] -N1Loader::Preloader.new(users).preload(:orders_count) -users.map(&:orders_count) +gem 'n1_loader', require: 'n1_loader/ar_lazy_preload' ``` -### Lazy loading +## Standalone mode -```ruby -class User - include N1Loader::Loadable +Are you not working with [ActiveRecord][5]? N1Loader is ready to be used as standalone solution! ([full snippet](examples/core.rb)) - # with inline loader - n1_optimized :orders_count do |users| - orders_per_user = Order.where(user: users).group(:user_id).count - - users.each { |user| fulfill(user, orders_per_user[user.id]) } - end -end - -user = User.new # => nothing was done for loading -user.orders_count # => first time loading - -users = [User.new, User.new] # => nothing was done for loading -N1Loader::Preloader.new([users]).preload(:orders_count) # => we only initialized loader but didn't perform it yet -users.map(&:orders_count) # => loading has happen for the first time (without N+1) -``` - - -### Shareable loaders - ```ruby -class OrdersCountLoader < N1Loader::Loader - def perform(users) - orders_per_user = Order.where(user: users).group(:user_id).count - - users.each { |user| fulfill(user, orders_per_user[user.id]) } - end -end - -class User - include N1Loader::Loadable - - n1_optimized :orders_count, OrdersCountLoader -end - -class Customer - include N1Loader::Loadable - - n1_optimized :orders_count, OrdersCountLoader -end - -User.new.orders_count # => works -Customer.new.orders_count # => works +gem 'n1_loader' ``` -### Reloading +## How to use it? -```ruby -class User - include N1Loader::Loadable +N1Loader provides DSL that allows you to define N+1 ready loaders that can +be injected into your objects in a way that you can avoid N+1 issues. - # with inline loader - n1_optimized :orders_count do |users| - orders_per_user = Order.where(user: users).group(:user_id).count +> _Disclaimer_: examples below are working but designed to show N1Loader potentials only. +In real live applications, N1Loader can be applied anywhere and in more [elegant way](examples/isolated_loader.rb). - users.each { |user| fulfill(user, orders_per_user[user.id]) } - end -end - -user = User.new -user.orders_count # => loader is executed first time and value was cached -user.orders_count(reload: true) # => loader is executed again and a new value was cached -# or -user.n1_clear_cache -user.orders_count - -users = [User.new, User.new] -N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized but not yet executed -users.map(&:orders_count) # => loader was executed first time without N+1 issue and values were cached - -N1Loader::Preloader.new(users).preload(:orders_count) # => loader was initialized again but not yet executed -users.map(&:orders_count) # => new loader was executed first time without N+1 issue and new values were cached -``` - -### Isolated loaders - +Let's look at simple example below ([full snippet](examples/active_record_integration.rb)): ```ruby -class IsolatedLoader < N1Loader::Loader - def perform(elements) - elements.each { |element| fulfill(element, [element]) } - end -end +class User < ActiveRecord::Base + has_many :payments -objects = [1, 2, 3, 4] -loader = IsolatedLoader.new(objects) -objects.each do |object| - loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class -end -``` + n1_optimized :payments_total do |users| + total_per_user = + Payment.group(:user_id) + .where(user: users) + .sum(:amount) + .tap { |h| h.default = 0 } -### Optimized single case - -```ruby -class User - include N1Loader::Loadable - - n1_optimized :orders_count do # no arguments passed to the block, so we can override both perform and single. - def perform(users) - orders_per_user = Order.where(user: users).group(:user_id).count - - users.each { |user| fulfill(user, orders_per_user[user.id]) } + users.each do |user| + total = total_per_user[user.id] + fulfill(user, total) end - - # Optimized for single object loading - def single(user) - user.orders.count - end end end -user = User.new -user.orders_count # single will be used here +class Payment < ActiveRecord::Base + belongs_to :user -users = [User.new, User.new] -N1Loader::Preloader.new(users).preload(:orders_count) -users.map(&:orders_count) # perform will be used once without N+1 -``` + validates :amount, presence: true +end -### Arguments +# A user has many payments. +# Assuming, we want to know for group of users, what is a total of their payments, we can do the following: -```ruby -class User - include N1Loader::Loadable +# Has N+1 issue +p User.all.map { |user| user.payments.sum(&:amount) } - n1_optimized :orders_count do - argument :type - - def perform(users) - orders_per_user = Order.where(type: type, user: users).group(:user_id).count - - users.each { |user| fulfill(user, orders_per_user[user.id]) } - end - end -end +# Has no N+1 but we load too many data that we don't actually need +p User.all.includes(:payments).map { |user| user.payments.sum(&:amount) } -user = User.new -user.orders_count(type: :gifts) # The loader will be performed first time for this argument -user.orders_count(type: :sales) # The loader will be performed first time for this argument -user.orders_count(type: :gifts) # The cached value will be used - -users = [User.new, User.new] -N1Loader::Preloader.new(users).preload(:orders_count) -users.map { |user| user.orders_count(type: :gifts) } # No N+1 here +# Has no N+1 and we load only what we need +p User.all.includes(:payments_total).map { |user| user.payments_total } ``` -_Note_: By default, we use `arguments.map(&:object_id)` to identify arguments but in some cases, -you may want to override it, for example: +Let's assume now, that we want to calculate the total of payments for the given period for a group of users. +N1Loader can do that as well! ([full snippet](examples/arguments_support.rb)) ```ruby -class User - include N1Loader::Loadable +class User < ActiveRecord::Base + has_many :payments - n1_optimized :orders_count do - argument :sale, optional: true, default: -> { Sale.last } - - cache_key { sale.id } - + n1_optimized :payments_total do + argument :from + argument :to + def perform(users) - orders_per_user = Order.where(sale: sale, user: users).group(:user_id).count - - users.each { |user| fulfill(user, orders_per_user[user.id]) } + total_per_user = + Payment + .group(:user_id) + .where(created_at: from..to) + .where(user: users) + .sum(:amount) + .tap { |h| h.default = 0 } + + users.each do |user| + total = total_per_user[user.id] + fulfill(user, total) + end end end end -user = User.new -user.orders_count(sale: Sale.first) # perform will be executed and value will be cached -user.orders_count(sale: Sale.first) # the cached value will be returned -``` +class Payment < ActiveRecord::Base + belongs_to :user - -## Integrations - -### [ActiveRecord][5] - -_Note_: Rails 7 support is coming soon! Stay tuned! - -```ruby -class User < ActiveRecord::Base - include N1Loader::Loadable - - n1_optimized :orders_count do |users| - orders_per_user = Order.where(user: users).group(:user_id).count - - users.each { |user| fulfill(user, orders_per_user[user.id]) } - end + validates :amount, presence: true end -# For single user -user = User.first -user.orders_count +# Has N+1 +p User.all.map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) } -# For many users without N+1 -User.limit(5).includes(:orders_count).map(&:orders_count) +# Has no N+1 but we load too many data that we don't need +p User.all.includes(:payments).map { |user| user.payments.select { |payment| payment.created_at >= from && payment.created_at <= to }.sum(&:amount) } -# or with explicit preloader -users = User.limit(5).to_a -N1Loader::Preloader.new(users).preload(:orders_count) - -# No N+1 here -users.map(&:orders_count) +# Has no N+1 and calculation is the most efficient +p User.all.includes(:payments_total).map { |user| user.payments_total(from: from, to: to) } ``` -### [ArLazyPreload][6] +## Features and benefits -```ruby -class User < ActiveRecord::Base - include N1Loader::Loadable - - n1_optimized :orders_count do |users| - orders_per_user = Order.where(user: users).group(:user_id).count - - users.each { |user| fulfill(user, orders_per_user[user.id]) } - end -end - -# For single user -user = User.first -user.orders_count - -# For many users without N+1 -User.lazy_preload(:orders_count).all.map(&:orders_count) -# or -User.preload_associations_lazily.all.map(&:orders_count) -# or -ArLazyPreload.config.auto_preload = true -User.all.map(:orders_count) -``` +- N1Loader doesn't use Promises which means it's easy to debug +- Doesn't require injection to objects, can be used in [isolation](examples/isolated_loader.rb) +- Loads data [lazily](examples/lazy_loading.rb) +- Loaders can be [shared](examples/shared_loader.rb) between multiple classes +- Loaded data can be [re-fetched](examples/reloading.rb) +- Loader can be optimized for [single cases](examples/single_case.rb) +- Loader support [arguments](examples/arguments_support.rb) +- Has [integration](examples/active_record_integration.rb) with [ActiveRecord][5] which makes it brilliant +- Has [integration](examples/ar_lazy_integration.rb) with [ArLazyPreload][6] which makes it excellent ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/djezzzl/n1_loader. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md). \ No newline at end of file