eager_counting 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +80 -0
  4. data/Rakefile +34 -0
  5. data/lib/eager_counting.rb +4 -0
  6. data/lib/eager_counting/count_by.rb +87 -0
  7. data/lib/eager_counting/version.rb +3 -0
  8. data/test/dummy/README.rdoc +28 -0
  9. data/test/dummy/Rakefile +6 -0
  10. data/test/dummy/app/assets/javascripts/application.js +13 -0
  11. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  12. data/test/dummy/app/controllers/application_controller.rb +5 -0
  13. data/test/dummy/app/helpers/application_helper.rb +2 -0
  14. data/test/dummy/app/models/action.rb +6 -0
  15. data/test/dummy/app/models/comment.rb +5 -0
  16. data/test/dummy/app/models/country.rb +3 -0
  17. data/test/dummy/app/models/place.rb +6 -0
  18. data/test/dummy/app/models/product.rb +3 -0
  19. data/test/dummy/app/models/user.rb +3 -0
  20. data/test/dummy/app/models/visit.rb +8 -0
  21. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  22. data/test/dummy/bin/bundle +3 -0
  23. data/test/dummy/bin/rails +4 -0
  24. data/test/dummy/bin/rake +4 -0
  25. data/test/dummy/bin/setup +29 -0
  26. data/test/dummy/config.ru +4 -0
  27. data/test/dummy/config/application.rb +26 -0
  28. data/test/dummy/config/boot.rb +5 -0
  29. data/test/dummy/config/database.yml +25 -0
  30. data/test/dummy/config/environment.rb +5 -0
  31. data/test/dummy/config/environments/development.rb +41 -0
  32. data/test/dummy/config/environments/production.rb +79 -0
  33. data/test/dummy/config/environments/test.rb +42 -0
  34. data/test/dummy/config/initializers/assets.rb +11 -0
  35. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  36. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  37. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  38. data/test/dummy/config/initializers/inflections.rb +16 -0
  39. data/test/dummy/config/initializers/mime_types.rb +4 -0
  40. data/test/dummy/config/initializers/session_store.rb +3 -0
  41. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  42. data/test/dummy/config/locales/en.yml +23 -0
  43. data/test/dummy/config/routes.rb +56 -0
  44. data/test/dummy/config/secrets.yml +22 -0
  45. data/test/dummy/db/development.sqlite3 +0 -0
  46. data/test/dummy/db/migrate/20150727081137_create_users.rb +8 -0
  47. data/test/dummy/db/migrate/20150727081314_create_countries.rb +8 -0
  48. data/test/dummy/db/migrate/20150727081755_create_places.rb +9 -0
  49. data/test/dummy/db/migrate/20150727081827_create_visits.rb +10 -0
  50. data/test/dummy/db/migrate/20150727081904_create_products.rb +8 -0
  51. data/test/dummy/db/migrate/20150727081905_create_actions.rb +10 -0
  52. data/test/dummy/db/migrate/20150727082016_create_comments.rb +9 -0
  53. data/test/dummy/db/schema.rb +68 -0
  54. data/test/dummy/db/test.sqlite3 +0 -0
  55. data/test/dummy/log/development.log +110 -0
  56. data/test/dummy/log/test.log +1437 -0
  57. data/test/dummy/public/404.html +67 -0
  58. data/test/dummy/public/422.html +67 -0
  59. data/test/dummy/public/500.html +66 -0
  60. data/test/dummy/public/favicon.ico +0 -0
  61. data/test/dummy/test/models/action_test.rb +7 -0
  62. data/test/dummy/test/models/comment_test.rb +7 -0
  63. data/test/dummy/test/models/country_test.rb +7 -0
  64. data/test/dummy/test/models/place_test.rb +7 -0
  65. data/test/dummy/test/models/product_test.rb +7 -0
  66. data/test/dummy/test/models/visit_test.rb +7 -0
  67. data/test/eager_counting_test.rb +85 -0
  68. data/test/test_helper.rb +19 -0
  69. metadata +257 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 782fb92403f0813636efd75ebce1f29c15c8471e
4
+ data.tar.gz: 57f313359f23e84271d7ed34457f360200193b64
5
+ SHA512:
6
+ metadata.gz: 120cc93ef8078e302b3e5f640c1e6233dd646ae9c688e0e876cc3084220498c4cd24bd052620902a270eaa95d5389575b72350eb049fe1c306f376343c8aa41c
7
+ data.tar.gz: 327228c462d576bb4140b8defe8d460907ca3c50a7e8c9d1a5bb9145e44e3236d15c7e8a0c280f0d1fb7207ac26fac087cc43171c203679e4e33ba93e44ecbdc
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Tim 'S.D.Eagle' Zeitz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # EagerCounting
2
+ Avoid N+1 Queries caused by `count` calls!
3
+
4
+ ## Installation
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem 'eager_counting'
10
+ ```
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install eager_counting
19
+
20
+ ## Usage
21
+
22
+ Include the `EagerCounting::CountBy` module on any `ActiveRecord` class.
23
+
24
+ ```ruby
25
+ class Comment < ActiveRecord::Base
26
+ include EagerCounting::CountBy
27
+ belongs_to :author
28
+ belongs_to :commentable, polymorphic: true
29
+ end
30
+ ```
31
+
32
+ This will allow you to call `count_by` on scopes of this class.
33
+ `count_by` performs a count grouped by the given association.
34
+ This means it will return a hash mapping the ids of the associated objects
35
+ to the number of rows this class has for each of them.
36
+
37
+ ### Examples:
38
+
39
+ ```ruby
40
+ Comment.count_by(:author) # => hash with author id mapped to number of comments this user made
41
+ ```
42
+
43
+ You can call this method on any relation object of the class you included it on
44
+
45
+ ```ruby
46
+ Comment.where(spam: false).count_by(:author) # => will only count non spam comments
47
+ ```
48
+
49
+ With the second argument you can also limit the scope of the association by which to count
50
+
51
+ ```ruby
52
+ Comment.count_by(:author, User.where(admin: false)) # => only count comments by non admin users
53
+ ```
54
+
55
+ By passing an hash as the association you can count by joined associations
56
+
57
+ ```ruby
58
+ Comment.count_by(author: { city: :country }) # => count comments by the country their from
59
+ ```
60
+
61
+ You can also use it on polymoprhic associations.
62
+ For that the second parameter is necessary to select the type of things to count by.
63
+
64
+ ```ruby
65
+ Comment.count_by(:commentable, Picture.all) # => how many comments does each picture have?
66
+ ```
67
+
68
+ ## Development
69
+
70
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
71
+
72
+ 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).
73
+
74
+ ## Contributing
75
+
76
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/foobar. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
77
+
78
+ ## License
79
+
80
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'EagerCounting'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,4 @@
1
+ require 'eager_counting/count_by'
2
+
3
+ module EagerCounting
4
+ end
@@ -0,0 +1,87 @@
1
+ require 'active_support/concern'
2
+
3
+ module EagerCounting
4
+ ##
5
+ # This module contains the ::count_by method.
6
+ # Include it to enable ::count_by on your model class
7
+ module CountBy
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+
12
+ ##
13
+ # Performs a count grouped by the given association.
14
+ # This means it will return a hash mapping the ids of the associated objects
15
+ # to the number of rows this class has for each of them.
16
+ #
17
+ # Example:
18
+ #
19
+ # class Comment < ActiveRecord::Base
20
+ # include EagerCounting::CountBy
21
+ # belongs_to :author
22
+ # belongs_to :commentable, polymorphic: true
23
+ # end
24
+ #
25
+ # Comment.count_by(:author) # => hash with author id mapped to number of comments this user made
26
+ #
27
+ # You can call this method on any relation object of the class you included it on
28
+ #
29
+ # Comment.where(spam: false).count_by(:author) # => will only count non spam comments
30
+ #
31
+ # With the second argument you can also limit the scope of the association by which to count
32
+ #
33
+ # Comment.count_by(:author, User.where(admin: false)) # => only count comments by non admin users
34
+ #
35
+ # By passing an hash as the association you can count by joined associations
36
+ #
37
+ # Comment.count_by(author: { city: :country }) # => count comments by the country their from
38
+ #
39
+ # You can also use it on polymoprhic associations.
40
+ # For that the second parameter is necessary to select the type of things to count by.
41
+ #
42
+ # Comment.count_by(:commentable, Picture.all) # => how many comments does each picture have?
43
+ #
44
+ def count_by(association_target, scope = nil)
45
+ association = deepest_value(association_target).to_s
46
+ scope ||= association.camelize.constantize.all
47
+ join = without_deepest_value(association_target)
48
+ target_model = self
49
+
50
+ if association_target.is_a? Hash
51
+ target_model = deepest_value(join).to_s.singularize.camelize.constantize
52
+ end
53
+
54
+ query = joins(join)
55
+ .merge(target_model.where(association => scope))
56
+ .group(association_column_name(target_model, association))
57
+ .count
58
+
59
+ Hash.new(0).merge query
60
+ end
61
+
62
+ private
63
+
64
+ def association_column_name(klass, association)
65
+ "#{klass.table_name}.#{klass.reflections[association].foreign_key}"
66
+ end
67
+
68
+ def without_deepest_value(map)
69
+ return {} unless map.is_a? Hash
70
+ key, value = map.to_a.first
71
+ if value.is_a? Hash
72
+ { key => without_deepest_value(value) }
73
+ else
74
+ key
75
+ end
76
+ end
77
+
78
+ def deepest_value(value)
79
+ if value.is_a? Hash
80
+ deepest_value(value.values.first)
81
+ else
82
+ value
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,3 @@
1
+ module EagerCounting
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Prevent CSRF attacks by raising an exception.
3
+ # For APIs, you may want to use :null_session instead.
4
+ protect_from_forgery with: :exception
5
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,6 @@
1
+ class Action < ActiveRecord::Base
2
+ include EagerCounting::CountBy
3
+
4
+ belongs_to :visit
5
+ belongs_to :product
6
+ end
@@ -0,0 +1,5 @@
1
+ class Comment < ActiveRecord::Base
2
+ include EagerCounting::CountBy
3
+
4
+ belongs_to :commentable, polymorphic: true
5
+ end
@@ -0,0 +1,3 @@
1
+ class Country < ActiveRecord::Base
2
+ has_many :places
3
+ end
@@ -0,0 +1,6 @@
1
+ class Place < ActiveRecord::Base
2
+ belongs_to :country
3
+
4
+ has_many :comments, as: :commentable
5
+ has_many :visits
6
+ end
@@ -0,0 +1,3 @@
1
+ class Product < ActiveRecord::Base
2
+ has_many :actions
3
+ end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :visits
3
+ end
@@ -0,0 +1,8 @@
1
+ class Visit < ActiveRecord::Base
2
+ include EagerCounting::CountBy
3
+
4
+ belongs_to :place
5
+ belongs_to :user
6
+
7
+ has_many :actions
8
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6
+ <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
3
+ require_relative '../config/boot'
4
+ require 'rails/commands'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../config/boot'
3
+ require 'rake'
4
+ Rake.application.run
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pathname'
3
+
4
+ # path to your application root.
5
+ APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6
+
7
+ Dir.chdir APP_ROOT do
8
+ # This script is a starting point to setup your application.
9
+ # Add necessary setup steps to this file:
10
+
11
+ puts "== Installing dependencies =="
12
+ system "gem install bundler --conservative"
13
+ system "bundle check || bundle install"
14
+
15
+ # puts "\n== Copying sample files =="
16
+ # unless File.exist?("config/database.yml")
17
+ # system "cp config/database.yml.sample config/database.yml"
18
+ # end
19
+
20
+ puts "\n== Preparing database =="
21
+ system "bin/rake db:setup"
22
+
23
+ puts "\n== Removing old logs and tempfiles =="
24
+ system "rm -f log/*"
25
+ system "rm -rf tmp/cache"
26
+
27
+ puts "\n== Restarting application server =="
28
+ system "touch tmp/restart.txt"
29
+ end
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails.application