bulk_loader 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +21 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Guardfile +75 -0
- data/README.md +127 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bulk_loader.gemspec +30 -0
- data/example/sample.rb +89 -0
- data/lib/bulk_loader/attribute.rb +32 -0
- data/lib/bulk_loader/class_attribute.rb +55 -0
- data/lib/bulk_loader/dsl.rb +40 -0
- data/lib/bulk_loader/lazy.rb +33 -0
- data/lib/bulk_loader/loader.rb +67 -0
- data/lib/bulk_loader/version.rb +5 -0
- data/lib/bulk_loader.rb +12 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 291b02e009a7794d095870421c89543837ad2333
|
4
|
+
data.tar.gz: 186c8a0ab04583ccc99bdfe79769372d1fb34321
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a5b8fef4a1dbcc0c01fd242dd6d78bbd52ffd0af490e934fe6b7f868f081f0986b43eac89b994e8ef2b8cc24420f6317d952e60a074468eef6d5ed2bc0846893
|
7
|
+
data.tar.gz: 41d078dca4196b95424f955d30e6da695fe7617dfd64f88e637c2a573e14b717a31ff4c7d73106360589d64f352a706d883499c7c98f655b847fd7558d5f9a11
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# See default settings https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml
|
2
|
+
AllCops:
|
3
|
+
TargetRubyVersion: 2.3
|
4
|
+
DisplayCopNames: true
|
5
|
+
Exclude:
|
6
|
+
- '*.gemspec'
|
7
|
+
- 'Gemfile'
|
8
|
+
- 'Guardfile'
|
9
|
+
- 'bin/*'
|
10
|
+
- 'Rakefile'
|
11
|
+
- 'example/**/*'
|
12
|
+
|
13
|
+
Metrics/BlockLength:
|
14
|
+
Exclude:
|
15
|
+
- 'spec/**/*'
|
16
|
+
|
17
|
+
Metrics/LineLength:
|
18
|
+
Max: 100
|
19
|
+
|
20
|
+
Style/Documentation:
|
21
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
19
|
+
# rspec may be run, below are examples of the most common uses.
|
20
|
+
# * bundler: 'bundle exec rspec'
|
21
|
+
# * bundler binstubs: 'bin/rspec'
|
22
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
23
|
+
# installed the spring binstubs per the docs)
|
24
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
25
|
+
# * 'just' rspec: 'rspec'
|
26
|
+
|
27
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
28
|
+
require "guard/rspec/dsl"
|
29
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
30
|
+
|
31
|
+
# Feel free to open issues for suggestions and improvements
|
32
|
+
|
33
|
+
# RSpec files
|
34
|
+
rspec = dsl.rspec
|
35
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
36
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
37
|
+
watch(rspec.spec_files)
|
38
|
+
|
39
|
+
# Ruby files
|
40
|
+
ruby = dsl.ruby
|
41
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
42
|
+
|
43
|
+
# Rails files
|
44
|
+
rails = dsl.rails(view_extensions: %w(erb haml slim))
|
45
|
+
dsl.watch_spec_files_for(rails.app_files)
|
46
|
+
dsl.watch_spec_files_for(rails.views)
|
47
|
+
|
48
|
+
watch(rails.controllers) do |m|
|
49
|
+
[
|
50
|
+
rspec.spec.call("routing/#{m[1]}_routing"),
|
51
|
+
rspec.spec.call("controllers/#{m[1]}_controller"),
|
52
|
+
rspec.spec.call("acceptance/#{m[1]}")
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Rails config changes
|
57
|
+
watch(rails.spec_helper) { rspec.spec_dir }
|
58
|
+
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
59
|
+
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
60
|
+
|
61
|
+
# Capybara features specs
|
62
|
+
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
|
63
|
+
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
|
64
|
+
|
65
|
+
# Turnip features and steps
|
66
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
67
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
68
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
guard :rubocop do
|
73
|
+
watch(%r{.+\.rb$})
|
74
|
+
watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
|
75
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# BulkLoader
|
2
|
+
|
3
|
+
## Example
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
# app/models/post.rb
|
7
|
+
|
8
|
+
class Post < ApplicationRecord
|
9
|
+
include BulkLoader::DSL
|
10
|
+
|
11
|
+
bulk_loader :comment_count, :id, default: 0 do |ids|
|
12
|
+
Comment.where(id: ids).group(:post_id).count
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
```
|
17
|
+
|
18
|
+
You can use this like followings:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
# app/controllers/posts_controller.rb
|
22
|
+
class PostsController < ApplicationController
|
23
|
+
def index
|
24
|
+
@posts = Post.limit(10)
|
25
|
+
|
26
|
+
# load comment_count blocks with mapping by #id
|
27
|
+
# you can avoid N+1 queries.
|
28
|
+
Post.bulk_loader.load(:comment_count, @posts)
|
29
|
+
|
30
|
+
render(json: @posts.map {|post| { id: post.id, comment_count: post.comment_count } })
|
31
|
+
end
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
## Description
|
36
|
+
|
37
|
+
BulkLoader::DSL only create bulk\_loader class method and bulk\_loader method.
|
38
|
+
So you can use with any object that is not ActiveRecord.
|
39
|
+
|
40
|
+
### Defining bulk\_loader method
|
41
|
+
|
42
|
+
If you include BulkLoader::DSL, you can use bulk\_loader class method.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class YourModel
|
46
|
+
include BulkLoader::DSL
|
47
|
+
|
48
|
+
bulk_loader :name, :mapped_key, default: nil do |mapped_keys|
|
49
|
+
# something with mapped_keys
|
50
|
+
{
|
51
|
+
mapped_key => value, # you should return Hash that has mapped_key as key.
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
that create a instance method that name is :name.
|
58
|
+
|
59
|
+
#### mapped\_key
|
60
|
+
|
61
|
+
mapped\_key is Symbol or Proc. if you want to use original object, you can pass `->(your_model) { your_model } `.
|
62
|
+
|
63
|
+
#### default option
|
64
|
+
|
65
|
+
If block's result return Hash that cannot mapped to original object, you can return value using default option.
|
66
|
+
If you does not pass default option, default is nil.
|
67
|
+
|
68
|
+
If you want to pass object like Array, you should use with lambda.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class YourModel
|
72
|
+
include BulkLoader::DSL
|
73
|
+
|
74
|
+
bulk_loader :name, :mapped_key, default: -> { [] } do |mapped_keys|
|
75
|
+
# something with mapped_keys
|
76
|
+
{
|
77
|
+
mapped_key => value, # you should return Hash that has mapped_key as key.
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
#### export: false
|
84
|
+
|
85
|
+
:name method is just shorthand for bulk\_loader.public\_send(:name). So if you not want to create :name method, you can pass export: false to bulk\_loader definition.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class YourModel
|
89
|
+
include BulkLoader::DSL
|
90
|
+
|
91
|
+
bulk_loader :name, :mapped_key, default: nil, export: false do |mapped_keys|
|
92
|
+
# something with mapped_keys
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
then you can use this like followings.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
YourModel.new.bulk_loader.name
|
101
|
+
```
|
102
|
+
|
103
|
+
## Installation
|
104
|
+
|
105
|
+
Add this line to your application's Gemfile:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
gem 'bulk_loader'
|
109
|
+
```
|
110
|
+
|
111
|
+
And then execute:
|
112
|
+
|
113
|
+
$ bundle
|
114
|
+
|
115
|
+
Or install it yourself as:
|
116
|
+
|
117
|
+
$ gem install bulk_loader
|
118
|
+
|
119
|
+
## Development
|
120
|
+
|
121
|
+
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.
|
122
|
+
|
123
|
+
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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
124
|
+
|
125
|
+
## Contributing
|
126
|
+
|
127
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/walf443/bulk_loader.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "bulk_loader"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/bulk_loader.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "bulk_loader/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bulk_loader"
|
8
|
+
spec.version = BulkLoader::VERSION
|
9
|
+
spec.authors = ["Keiji Yoshimi"]
|
10
|
+
spec.email = ["walf443@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{utility to avoid N+1 queries}
|
13
|
+
spec.description = %q{utility to avoid N+1 queries}
|
14
|
+
spec.homepage = "https://github.com/walf443/bulk_loader"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
spec.add_development_dependency "rubocop", "~> 0.49"
|
27
|
+
spec.add_development_dependency "guard", "~> 2.14"
|
28
|
+
spec.add_development_dependency "guard-rspec", "~> 4.7.3"
|
29
|
+
spec.add_development_dependency "guard-rubocop", "~> 1.3.0"
|
30
|
+
end
|
data/example/sample.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require File.join(File.expand_path(File.join(__dir__, '..')), 'lib', 'bulk_loader')
|
2
|
+
require 'singleton'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
class CommentRepositoy
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@comments = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def push(comment)
|
13
|
+
@comments.push(comment)
|
14
|
+
end
|
15
|
+
|
16
|
+
def load
|
17
|
+
@comments
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Comment
|
22
|
+
attr_accessor :id, :post_id, :user_id
|
23
|
+
|
24
|
+
def initialize(id:, post_id:, user_id:)
|
25
|
+
self.id = id
|
26
|
+
self.user_id = user_id
|
27
|
+
self.post_id = post_id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Post
|
32
|
+
include BulkLoader::DSL
|
33
|
+
attr_accessor :id
|
34
|
+
|
35
|
+
bulk_loader :comment_count, :id, default: 0 do |ids|
|
36
|
+
p "try loading :comment_count with #{ids.inspect}"
|
37
|
+
CommentRepositoy.instance.load.each_with_object(Hash.new(0)) {|e, a| a[e.post_id] += 1 }
|
38
|
+
end
|
39
|
+
|
40
|
+
bulk_loader :comment_user_count, :id, default: 0 do |ids|
|
41
|
+
p "try loading :comment_user_count with #{ids.inspect}"
|
42
|
+
CommentRepositoy.instance.load
|
43
|
+
.each_with_object({}) {|e, a| a[e.post_id] ||= Set.new; a[e.post_id].add(e.user_id) }
|
44
|
+
.each_with_object({}){ |(key, user_ids), a| a[key] = user_ids.size }
|
45
|
+
end
|
46
|
+
|
47
|
+
bulk_loader :comment_exist?, :id, default: false do |ids|
|
48
|
+
p "try loading :comment_exist? with #{ids.inspect}"
|
49
|
+
CommentRepositoy.instance.load.each_with_object({}) {|e, a| a[e.post_id] = true }
|
50
|
+
end
|
51
|
+
|
52
|
+
bulk_loader :commented?, :id, default: false do |ids, current_user|
|
53
|
+
CommentRepositoy.instance.load.each_with_object({}) {|e, a| a[e.post_id] = true if e.user_id == current_user&.id }
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(id:)
|
57
|
+
self.id = id
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class User
|
62
|
+
attr_accessor :id
|
63
|
+
def initialize(id:)
|
64
|
+
self.id = id
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
repository = CommentRepositoy.instance
|
69
|
+
post1 = Post.new(id: 1)
|
70
|
+
post2 = Post.new(id: 2)
|
71
|
+
post3 = Post.new(id: 3)
|
72
|
+
|
73
|
+
repository.push(Comment.new(id: 1, user_id: 1, post_id: post1.id))
|
74
|
+
repository.push(Comment.new(id: 2, user_id: 1, post_id: post1.id))
|
75
|
+
repository.push(Comment.new(id: 3, user_id: 2, post_id: post1.id))
|
76
|
+
repository.push(Comment.new(id: 4, user_id: 1, post_id: post2.id))
|
77
|
+
repository.push(Comment.new(id: 5, user_id: 2, post_id: post2.id))
|
78
|
+
|
79
|
+
Post.bulk_loader.load([:comment_count, :comment_user_count, :comment_exist?], [post1, post2, post3, post1])
|
80
|
+
# p post1.bulk_loader.comment_count
|
81
|
+
p post1.comment_count
|
82
|
+
p post2.comment_count
|
83
|
+
p post1.comment_exist?
|
84
|
+
p post3.comment_exist?
|
85
|
+
p post1.comment_user_count
|
86
|
+
|
87
|
+
current_user = User.new(id: 1)
|
88
|
+
Post.bulk_loader.load(:commented?, [post1, post2, post3], nil)
|
89
|
+
p post1.commented?
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulkLoader
|
4
|
+
class Attribute
|
5
|
+
def initialize(cattr, target)
|
6
|
+
@class_attribute = cattr
|
7
|
+
@target = target
|
8
|
+
@lazy_of ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def lazy(name)
|
12
|
+
@lazy_of[name] ||= BulkLoader::Lazy.new(@target)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def method_missing(name, *args)
|
18
|
+
return super unless @class_attribute.include?(name)
|
19
|
+
define_singleton_method(name) do
|
20
|
+
attr = lazy(name)
|
21
|
+
@class_attribute.load([name], [self]) unless attr.loaded?
|
22
|
+
attr.get
|
23
|
+
end
|
24
|
+
public_send(name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def respond_to_missing?(name, include_private)
|
28
|
+
return true if @class_attribute.include?(name)
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulkLoader
|
4
|
+
class ClassAttribute
|
5
|
+
def initialize
|
6
|
+
@loader_of = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def include?(name)
|
10
|
+
@loader_of.include?(name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(loader_names, attributes, *args)
|
14
|
+
attrs = convert_attributes(attributes)
|
15
|
+
|
16
|
+
loader_names = [loader_names] unless loader_names.is_a?(Array)
|
17
|
+
loader_names.each do |name|
|
18
|
+
@loader_of[name].load(attrs.map { |attr| attr.lazy(name) }, *args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def define_loader(name, loader)
|
23
|
+
@loader_of[name] = loader
|
24
|
+
define_singleton_method(name) { loader }
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
"#<BulkLoader::ClassAttribute:#{object_id} #{loader_inspect}>"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def loader_inspect
|
34
|
+
@loader_of.map { |name, _| name }.join(' ')
|
35
|
+
end
|
36
|
+
|
37
|
+
def convert_attributes(attributes)
|
38
|
+
attrs = []
|
39
|
+
attributes.each do |attr|
|
40
|
+
attrs.push(convert_attribute(attr))
|
41
|
+
end
|
42
|
+
attrs
|
43
|
+
end
|
44
|
+
|
45
|
+
def convert_attribute(attr)
|
46
|
+
if attr.respond_to?(:lazy)
|
47
|
+
attr
|
48
|
+
elsif attr.respond_to?(:bulk_loader)
|
49
|
+
attr.bulk_loader
|
50
|
+
else
|
51
|
+
raise 'attributes should be BulkLoader::Attribute or BulkLoader::DSL included class!!'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'weakref'
|
4
|
+
|
5
|
+
module BulkLoader
|
6
|
+
module DSL
|
7
|
+
module ClassMethods
|
8
|
+
# getter for +BulkLoader::ClassAttribute+
|
9
|
+
# If you pass name, mapping, options argument, you can define loader
|
10
|
+
# if you does not want to export name to object, pass export: false to options.
|
11
|
+
def bulk_loader(*args, &block)
|
12
|
+
@bulk_loader ||= BulkLoader::ClassAttribute.new
|
13
|
+
return @bulk_loader if args.empty?
|
14
|
+
|
15
|
+
name, mapping, options = *args
|
16
|
+
options ||= {}
|
17
|
+
does_export = options.delete(:export)
|
18
|
+
|
19
|
+
@bulk_loader.define_loader(name, BulkLoader::Loader.new(mapping, options, &block))
|
20
|
+
|
21
|
+
return if does_export == false
|
22
|
+
|
23
|
+
define_method name do
|
24
|
+
bulk_loader.public_send(name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.included(mod)
|
30
|
+
mod.extend(ClassMethods)
|
31
|
+
end
|
32
|
+
|
33
|
+
def bulk_loader
|
34
|
+
return @bulk_loader if @bulk_loader
|
35
|
+
|
36
|
+
class_attribute = self.class.bulk_loader
|
37
|
+
@bulk_loader = BulkLoader::Attribute.new(class_attribute, WeakRef.new(self))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulkLoader
|
4
|
+
# lazy class
|
5
|
+
class Lazy
|
6
|
+
attr_reader :target
|
7
|
+
|
8
|
+
def initialize(target)
|
9
|
+
@loaded = false
|
10
|
+
@value = nil
|
11
|
+
@target = target
|
12
|
+
end
|
13
|
+
|
14
|
+
def get
|
15
|
+
raise 'data is not loaded!!' unless @loaded
|
16
|
+
@value
|
17
|
+
end
|
18
|
+
|
19
|
+
def set(value)
|
20
|
+
@loaded = true
|
21
|
+
@value = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear
|
25
|
+
@loaded = false
|
26
|
+
@value = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def loaded?
|
30
|
+
@loaded
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BulkLoader
|
4
|
+
class Loader
|
5
|
+
# +mapping+ is a Symbol or Proc. block's 1st argument mapped using mapping.
|
6
|
+
# and your block's return value's key should be mapped value.
|
7
|
+
def initialize(mapping, default: nil, &block)
|
8
|
+
@mapping = mapping
|
9
|
+
@is_mapping_proc = @mapping.is_a?(Proc)
|
10
|
+
@default = default
|
11
|
+
@is_default_proc = @default.is_a?(Proc)
|
12
|
+
@block = block
|
13
|
+
end
|
14
|
+
|
15
|
+
def load(lazy_objs, *args)
|
16
|
+
lazy_obj_of = lazy_objs.each_with_object({}) { |e, h| h[e.target] = e }
|
17
|
+
|
18
|
+
mapping_of = get_mapping(lazy_objs)
|
19
|
+
|
20
|
+
result_of = call_block(mapping_of, *args)
|
21
|
+
|
22
|
+
lazy_objs.each(&:clear)
|
23
|
+
|
24
|
+
set_result_to_lazy_objs(result_of, lazy_obj_of, mapping_of)
|
25
|
+
|
26
|
+
fill_default_to_unloaded_obj(lazy_objs)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def call_block(mapping_of, *args)
|
32
|
+
if args.size < @block.arity - 1
|
33
|
+
message = "block should take #{@block.arity} parameters, but given #{arity.size + 1}"
|
34
|
+
raise ArgumentError, message
|
35
|
+
end
|
36
|
+
result_of = @block.call(mapping_of.keys, *args)
|
37
|
+
raise 'block shuold return Hash' unless result_of.is_a?(Hash)
|
38
|
+
result_of
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_mapping(lazy_objs)
|
42
|
+
mapping_of = {}
|
43
|
+
targets = lazy_objs.map(&:target)
|
44
|
+
targets.each do |target|
|
45
|
+
mapped_target = @is_mapping_proc ? @mapping.call(target) : target.public_send(@mapping)
|
46
|
+
mapping_of[mapped_target] = [] unless mapping_of[mapped_target]
|
47
|
+
mapping_of[mapped_target].push(target)
|
48
|
+
end
|
49
|
+
mapping_of
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_result_to_lazy_objs(result_of, lazy_obj_of, mapping_of)
|
53
|
+
result_of.each do |mapped_target, value|
|
54
|
+
next unless mapping_of[mapped_target]
|
55
|
+
mapping_of[mapped_target].each do |target|
|
56
|
+
lazy_obj_of[target]&.set(value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def fill_default_to_unloaded_obj(lazy_objs)
|
62
|
+
lazy_objs.each do |lazy_obj|
|
63
|
+
lazy_obj.set(@is_default_proc ? @default.call : @default) unless lazy_obj.loaded?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/bulk_loader.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bulk_loader/version'
|
4
|
+
require 'bulk_loader/loader'
|
5
|
+
require 'bulk_loader/lazy'
|
6
|
+
require 'bulk_loader/class_attribute'
|
7
|
+
require 'bulk_loader/attribute'
|
8
|
+
require 'bulk_loader/dsl'
|
9
|
+
|
10
|
+
module BulkLoader
|
11
|
+
# Your code goes here...
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bulk_loader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Keiji Yoshimi
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.49'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.49'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.14'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.14'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: guard-rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 4.7.3
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 4.7.3
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: guard-rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.3.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.3.0
|
111
|
+
description: utility to avoid N+1 queries
|
112
|
+
email:
|
113
|
+
- walf443@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rspec"
|
120
|
+
- ".rubocop.yml"
|
121
|
+
- ".travis.yml"
|
122
|
+
- Gemfile
|
123
|
+
- Guardfile
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- bin/console
|
127
|
+
- bin/setup
|
128
|
+
- bulk_loader.gemspec
|
129
|
+
- example/sample.rb
|
130
|
+
- lib/bulk_loader.rb
|
131
|
+
- lib/bulk_loader/attribute.rb
|
132
|
+
- lib/bulk_loader/class_attribute.rb
|
133
|
+
- lib/bulk_loader/dsl.rb
|
134
|
+
- lib/bulk_loader/lazy.rb
|
135
|
+
- lib/bulk_loader/loader.rb
|
136
|
+
- lib/bulk_loader/version.rb
|
137
|
+
homepage: https://github.com/walf443/bulk_loader
|
138
|
+
licenses: []
|
139
|
+
metadata: {}
|
140
|
+
post_install_message:
|
141
|
+
rdoc_options: []
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
requirements: []
|
155
|
+
rubyforge_project:
|
156
|
+
rubygems_version: 2.5.1
|
157
|
+
signing_key:
|
158
|
+
specification_version: 4
|
159
|
+
summary: utility to avoid N+1 queries
|
160
|
+
test_files: []
|