n1_loader 0.1.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e2a77c7fdcb24c0ad611468803c952e120c43b2c54be109828380d01723dd3f
4
- data.tar.gz: 37dfae873e6cba9e5539b4609d324ff3fc5489d5be305eec1bbac5389089379c
3
+ metadata.gz: 8a365bb6b58a962185cd24033f6dd6cb98510712973dacdea4977a3121b64adc
4
+ data.tar.gz: d06031503475dc13f52acddb881931de5b7dd244acab6b46553d4a73c8efbd2d
5
5
  SHA512:
6
- metadata.gz: 8fe48829416810c0fac586dbe4b5002e4de2906d5ed1d99813d094c77db29d1a941cf742d078463632a0802fecfea55cc091e469b7510d2d566a4f2acdb1413f
7
- data.tar.gz: 71038ccc2403d0baae719ebbd9a00ed7351e1370c255bf5bd3a5d826d4d6824323c53bd317d7193bc92a175b1786abb2b27a64b45e34d97c6427baaee3c58844
6
+ metadata.gz: 9172c4cd233739a0ebc379b3988037349d028020d5eb066642532f2b3da2a75d3faa34edd872aa5b5925eed91509c2c3a202e7a9aa448ca5020435b4f5e9c5a5
7
+ data.tar.gz: e74f92e69f47909b812adecb1aa7dc319c090a2ad865e89732a4ebdbbd378c02dbec28cc7ff5b0ff42262acf3a07e8deb8a4a949634d70c6d6c3986837a6ab5d
data/.circleci/config.yml CHANGED
@@ -86,8 +86,15 @@ workflows:
86
86
  "latest"
87
87
  ]
88
88
  gemfile: [
89
+ "gemfiles/ar_5_latest.gemfile",
89
90
  "gemfiles/ar_6_latest.gemfile"
90
91
  ]
92
+ exclude:
93
+ - ruby-version: "3.0"
94
+ gemfile: "gemfiles/ar_5_latest.gemfile"
95
+ - ruby-version: "latest"
96
+ gemfile: "gemfiles/ar_5_latest.gemfile"
97
+
91
98
  name: << matrix.gemfile >>-build-ruby-<< matrix.ruby-version >>
92
99
  - rubocop:
93
100
  requires:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
- ## [Unreleased]
1
+ ## [1.1.0] - 2021-12-27
2
+
3
+ - Introduce `fulfill` method to abstract the storage.
4
+
5
+ ## [1.0.0] - 2021-12-26
6
+
7
+ - Various of great features.
2
8
 
3
9
  ## [0.1.0] - 2021-12-16
4
10
 
5
- - Initial release
11
+ - Initial release.
data/README.md CHANGED
@@ -4,14 +4,20 @@
4
4
  [![Gem Version][3]][4]
5
5
 
6
6
  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?
7
- We have a solution for you! [N1Loader][8] is designed to solve the issue for good!
7
+ We have a solution for you!
8
+
9
+ [N1Loader][8] is designed to solve the issue for good!
8
10
 
9
11
  It has many benefits:
10
- - it loads data lazily (even when you initialized preloading)
11
- - it supports shared loaders between multiple classes
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)
12
17
  - it has an integration with [ActiveRecord][5] which makes it brilliant ([example](#activerecord))
13
18
  - it has an integration with [ArLazyPreload][6] which makes it excellent ([example](#arlazypreload))
14
19
 
20
+ ... and even more features to come! Stay tuned!
15
21
 
16
22
  ## Installation
17
23
 
@@ -23,24 +29,25 @@ gem 'n1_loader'
23
29
 
24
30
  You can add integration with [ActiveRecord][5] by:
25
31
  ```ruby
26
- require 'n1_loader/active_record'
32
+ gem 'n1_loader', require: 'n1_loader/active_record'
27
33
  ```
28
34
 
29
35
  You can add the integration with [ActiveRecord][5] and [ArLazyPreload][6] by:
30
36
  ```ruby
31
- require 'n1_loader/ar_lazy_preload'
37
+ gem 'n1_loader', require: 'n1_loader/ar_lazy_preload'
32
38
  ```
33
39
 
34
40
  ## Usage
35
41
 
42
+ **Supported Ruby version:** 2.5, 2.6, 2.7, 3.0, and latest.
43
+
36
44
  ```ruby
37
45
  class Example
38
46
  include N1Loader::Loadable
39
47
 
40
48
  # with inline loader
41
49
  n1_loader :anything do |elements|
42
- # Has to return a hash that has keys as element from elements
43
- elements.group_by(&:itself)
50
+ elements.each { |element| fulfill(element, [element]) }
44
51
  end
45
52
 
46
53
  # with custom loader
@@ -49,9 +56,8 @@ end
49
56
 
50
57
  # Custom loader that can be shared with many classes
51
58
  class MyLoader < N1Loader::Loader
52
- # Has to return a hash that has keys as element from elements
53
59
  def perform(elements)
54
- elements.group_by(&:itself)
60
+ elements.each { |element| fulfill(element, [element]) }
55
61
  end
56
62
  end
57
63
 
@@ -65,8 +71,127 @@ N1Loader::Preloader.new(objects).preload(:anything)
65
71
  objects.map(&:anything)
66
72
  ```
67
73
 
74
+ ### Lazy loading
75
+
76
+ ```ruby
77
+ class Example
78
+ include N1Loader::Loadable
79
+
80
+ n1_loader :anything do |elements|
81
+ elements.each { |element| fulfill(element, [element]) }
82
+ end
83
+ end
84
+
85
+ object = Example.new # => nothing was done for loading
86
+ object.anything # => first time loading
87
+
88
+ objects = [Example.new, Example.new] # => nothing was done for loading
89
+ N1Loader::Preloader.new([objects]).preload(:anything) # => we only initial loader but didn't perform it yet
90
+ objects.map(&:anything) # => loading happen for the first time (without N+1)
91
+ ```
92
+
93
+
94
+ ### Shareable loaders
95
+
96
+ ```ruby
97
+ class MyLoader < N1Loader::Loader
98
+ def perform(elements)
99
+ elements.each { |element| fulfill(element, [element]) }
100
+ end
101
+ end
102
+
103
+ class A
104
+ include N1Loader::Loadable
105
+
106
+ n1_loader :anything, MyLoader
107
+ end
108
+
109
+ class B
110
+ include N1Loader::Loadable
111
+
112
+ n1_loader :something, MyLoader
113
+ end
114
+
115
+ A.new.anything # => works
116
+ B.new.something # => works
117
+ ```
118
+
119
+ ### Reloading
120
+
121
+ ```ruby
122
+ class Example
123
+ include N1Loader::Loadable
124
+
125
+ # with inline loader
126
+ n1_loader :anything do |elements|
127
+ elements.each { |element| fulfill(element, [element]) }
128
+ end
129
+ end
130
+
131
+ object = Example.new
132
+ object.anything # => loader is executed first time and value was cached
133
+ object.anything(reload: true) # => loader is executed again and a new value was cached
134
+
135
+ objects = [Example.new, Example.new]
136
+
137
+ N1Loader::Preloader.new(objects).preload(:anything) # => loader was initialized but not yet executed
138
+ objects.map(&:anything) # => loader was executed first time without N+1 issue and values were cached
139
+
140
+ N1Loader::Preloader.new(objects).preload(:anything) # => loader was initialized again but not yet executed
141
+ objects.map(&:anything) # => new loader was executed first time without N+1 issue and new values were cached
142
+ ```
143
+
144
+ ### Isolated loaders
145
+
146
+ ```ruby
147
+ class MyLoader < N1Loader::Loader
148
+ def perform(elements)
149
+ elements.each { |element| fulfill(element, [element]) }
150
+ end
151
+ end
152
+
153
+ objects = [1, 2, 3, 4]
154
+ loader = MyLoader.new(objects)
155
+ objects.each do |object|
156
+ loader.for(object) # => it has no N+1 and it doesn't require to be injected in the class
157
+ end
158
+ ```
159
+
160
+ ### Optimized single case
161
+
162
+ ```ruby
163
+ class Example
164
+ include N1Loader::Loadable
165
+
166
+ n1_loader :something do # no arguments passed to the block, so we can override both perform and single.
167
+ def perform(elements)
168
+ elements.each { |element| fulfill(element, [element]) }
169
+ end
170
+
171
+ # Optimized for single object loading
172
+ def single(element)
173
+ # Just return a value you want to have for this element
174
+ [element]
175
+ end
176
+ end
177
+ end
178
+
179
+ object = Example.new
180
+ object.something # single will be used here
181
+
182
+ objects = [Example.new, Example.new]
183
+ N1Loader::Preloader.new(objects).preload(:something)
184
+ objects.map(&:something) # perform will be used once without N+1
185
+ ```
186
+
187
+ ## Integrations
188
+
68
189
  ### [ActiveRecord][5]
69
190
 
191
+ **Supported versions**: 5, 6.
192
+
193
+ _Note_: Please open an issue if you interested in support of version 7 or other.
194
+
70
195
  ```ruby
71
196
  class User < ActiveRecord::Base
72
197
  include N1Loader::Loadable
@@ -74,8 +199,7 @@ class User < ActiveRecord::Base
74
199
  n1_loader :orders_count do |users|
75
200
  hash = Order.where(user: users).group(:user_id).count
76
201
 
77
- # hash has to have keys as initial elements
78
- hash.transform_keys! { |key| users.find { |user| user.id == key } }
202
+ users.each { |user| fulfill(user, hash[user.id]) }
79
203
  end
80
204
  end
81
205
 
@@ -102,9 +226,8 @@ class User < ActiveRecord::Base
102
226
 
103
227
  n1_loader :orders_count do |users|
104
228
  hash = Order.where(user: users).group(:user_id).count
105
-
106
- # hash has to have keys as initial elements
107
- hash.transform_keys! { |key| users.find { |user| user.id == key } }
229
+
230
+ users.each { |user| fulfill(user, hash[user.id]) }
108
231
  end
109
232
  end
110
233
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5"
6
+
7
+ gemspec path: "../"
@@ -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
@@ -17,7 +17,9 @@ module N1Loader
17
17
  end
18
18
 
19
19
  def grouped_records(association, records, polymorphic_parent)
20
- n1_load_records, records = records.partition { |record| record.class.n1_loader_defined?(association) }
20
+ n1_load_records, records = records.partition do |record|
21
+ record.class.respond_to?(:n1_loader_defined?) && record.class.n1_loader_defined?(association)
22
+ end
21
23
 
22
24
  hash = n1_load_records.group_by do |record|
23
25
  N1LoaderReflection.new(association, record.class.n1_loader(association))
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ N1Loader::Loader.define_method :preloaded_records do
4
+ @preloaded_records ||= loaded.values
5
+ end
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../n1_loader"
4
+ require_relative "active_record/loader"
3
5
  require "active_record"
4
6
 
5
- require_relative "active_record/associations_preloader"
6
-
7
7
  ActiveSupport.on_load(:active_record) do
8
+ case ActiveRecord::VERSION::MAJOR
9
+ when 6
10
+ require_relative "active_record/associations_preloader_v6"
11
+ else
12
+ require_relative "active_record/associations_preloader_v5"
13
+ end
14
+
8
15
  ActiveRecord::Associations::Preloader.prepend(N1Loader::ActiveRecord::Associations::Preloader)
9
16
  end
@@ -6,7 +6,7 @@ module N1Loader
6
6
  class ContextAdapter
7
7
  attr_reader :context
8
8
 
9
- delegate :records, :auto_preload?, to: :context
9
+ delegate_missing_to :context
10
10
 
11
11
  def initialize(context)
12
12
  @context = context
@@ -8,9 +8,7 @@ module N1Loader
8
8
  #
9
9
  # # with inline loader
10
10
  # n1_loader :something do |elements|
11
- # elements.each_with_object({}) do |element, hash|
12
- # hash[element] = element.calculate_something
13
- # end
11
+ # elements.each { |element| fulfill(element,, element.calculate_something) }
14
12
  # end
15
13
  #
16
14
  # # with custom loader
@@ -20,9 +18,7 @@ module N1Loader
20
18
  # # custom loader
21
19
  # class MyLoader < N1Loader::Loader
22
20
  # def perform(elements)
23
- # elements.each_with_object({}) do |element, hash|
24
- # hash[element] = element.calculate_something
25
- # end
21
+ # elements.each { |element| fulfill(element,, element.calculate_something) }
26
22
  # end
27
23
  # end
28
24
  module Loadable
@@ -47,9 +43,13 @@ module N1Loader
47
43
  respond_to?("#{name}_loader")
48
44
  end
49
45
 
50
- def n1_load(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength
46
+ def n1_load(name, loader = nil, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
51
47
  loader ||= Class.new(N1Loader::Loader) do
52
- define_method(:perform, &block)
48
+ if block && block.arity == 1
49
+ define_method(:perform, &block)
50
+ else
51
+ class_eval(&block)
52
+ end
53
53
  end
54
54
 
55
55
  loader_name = "#{name}_loader"
@@ -59,16 +59,21 @@ module N1Loader
59
59
  loader
60
60
  end
61
61
 
62
+ define_method("#{loader_name}_reload") do
63
+ instance_variable_set(loader_variable_name, self.class.send(loader_name).new([self]))
64
+ end
65
+
62
66
  define_method("#{loader_name}=") do |loader_instance|
63
67
  instance_variable_set(loader_variable_name, loader_instance)
64
68
  end
65
69
 
66
70
  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]))
71
+ instance_variable_get(loader_variable_name) || send("#{loader_name}_reload")
69
72
  end
70
73
 
71
- define_method(name) do
74
+ define_method(name) do |reload: false|
75
+ send("#{loader_name}_reload") if reload
76
+
72
77
  send(loader_name).for(self)
73
78
  end
74
79
 
@@ -10,26 +10,39 @@ module N1Loader
10
10
  @elements = elements
11
11
  end
12
12
 
13
- def perform(_elements)
14
- raise NotImplemented, "Subclasses have to implement the method"
13
+ def for(element)
14
+ if loaded.empty? && elements.any?
15
+ raise NotFilled, "Nothing was preloaded, perhaps you forgot to use fulfill method"
16
+ end
17
+ raise NotLoaded, "The data was not preloaded for the given element" unless loaded.key?(element)
18
+
19
+ loaded[element]
15
20
  end
16
21
 
17
- def loaded
18
- @loaded ||= perform(elements)
22
+ private
23
+
24
+ attr_reader :elements
25
+
26
+ def perform(_elements)
27
+ raise NotImplemented, "Subclasses have to implement the method"
19
28
  end
20
29
 
21
- def preloaded_records
22
- @preloaded_records ||= loaded.values
30
+ def fulfill(element, value)
31
+ @loaded[element] = value
23
32
  end
24
33
 
25
- def for(element)
26
- raise NotLoaded, "The data was not preloaded for the given element" unless elements.include?(element)
34
+ def loaded
35
+ return @loaded if @loaded
27
36
 
28
- loaded.compare_by_identity[element]
29
- end
37
+ @loaded = {}.compare_by_identity
30
38
 
31
- private
39
+ if elements.size == 1 && respond_to?(:single)
40
+ fulfill(elements.first, single(elements.first))
41
+ elsif elements.any?
42
+ perform(elements)
43
+ end
32
44
 
33
- attr_reader :elements
45
+ @loaded
46
+ end
34
47
  end
35
48
  end
File without changes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module N1Loader
4
- VERSION = "0.1.0"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/n1_loader.rb CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  require_relative "n1_loader/version"
4
4
 
5
- require_relative "n1_loader/loader"
6
- require_relative "n1_loader/loadable"
7
- require_relative "n1_loader/preloader"
5
+ require_relative "n1_loader/core/loader"
6
+ require_relative "n1_loader/core/loadable"
7
+ require_relative "n1_loader/core/preloader"
8
8
 
9
9
  module N1Loader # :nodoc:
10
10
  class Error < StandardError; end
11
11
  class NotImplemented < Error; end
12
12
  class NotLoaded < Error; end
13
+ class NotFilled < Error; end
13
14
  end
data/n1_loader.gemspec CHANGED
@@ -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", "~> 6.0"
25
+ spec.add_development_dependency "activerecord", ">= 5"
26
26
  spec.add_development_dependency "ar_lazy_preload", "~> 0.7"
27
27
  spec.add_development_dependency "db-query-matchers", "~> 0.10"
28
- spec.add_development_dependency "rails", "~> 6.0"
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,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n1_loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.1.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: 2021-12-18 00:00:00.000000000 Z
11
+ date: 2021-12-27 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: '6.0'
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: '6.0'
26
+ version: '5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: ar_lazy_preload
29
29
  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: '6.0'
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: '6.0'
68
+ version: '5'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -141,17 +141,20 @@ files:
141
141
  - Rakefile
142
142
  - bin/console
143
143
  - bin/setup
144
+ - gemfiles/ar_5_latest.gemfile
144
145
  - gemfiles/ar_6_latest.gemfile
145
146
  - lib/n1_loader.rb
146
147
  - lib/n1_loader/active_record.rb
147
- - lib/n1_loader/active_record/associations_preloader.rb
148
+ - lib/n1_loader/active_record/associations_preloader_v5.rb
149
+ - lib/n1_loader/active_record/associations_preloader_v6.rb
150
+ - lib/n1_loader/active_record/loader.rb
148
151
  - lib/n1_loader/ar_lazy_preload.rb
149
152
  - lib/n1_loader/ar_lazy_preload/associated_context_builder.rb
150
153
  - lib/n1_loader/ar_lazy_preload/context_adapter.rb
151
154
  - lib/n1_loader/ar_lazy_preload/loadable.rb
152
- - lib/n1_loader/loadable.rb
153
- - lib/n1_loader/loader.rb
154
- - lib/n1_loader/preloader.rb
155
+ - lib/n1_loader/core/loadable.rb
156
+ - lib/n1_loader/core/loader.rb
157
+ - lib/n1_loader/core/preloader.rb
155
158
  - lib/n1_loader/version.rb
156
159
  - n1_loader.gemspec
157
160
  homepage: https://github.com/djezzzl/n1_loader