activerecord-precounter-0.4.0/0000755000175100017510000000000014125330214016526 5ustar vivekdebvivekdebactiverecord-precounter-0.4.0/lib/0000755000175100017510000000000014125330214017274 5ustar vivekdebvivekdebactiverecord-precounter-0.4.0/lib/activerecord-precounter.rb0000644000175100017510000000004314125330214024454 0ustar vivekdebvivekdebrequire 'active_record/precounter' activerecord-precounter-0.4.0/lib/active_record/0000755000175100017510000000000014125330214022105 5ustar vivekdebvivekdebactiverecord-precounter-0.4.0/lib/active_record/precounter/0000755000175100017510000000000014125330214024273 5ustar vivekdebvivekdebactiverecord-precounter-0.4.0/lib/active_record/precounter/version.rb0000644000175100017510000000010714125330214026303 0ustar vivekdebvivekdebmodule ActiveRecord class Precounter VERSION = '0.4.0' end end activerecord-precounter-0.4.0/lib/active_record/precounter.rb0000644000175100017510000000574414125330214024632 0ustar vivekdebvivekdebrequire 'active_record/precounter/version' require 'active_record/precountable' module ActiveRecord class Precounter class MissingInverseOf < StandardError; end # @param [ActiveRecord::Relation] relation - Parent resources relation. def initialize(relation) @relation = relation end # @param [Array] association_names - Eager loaded association names. e.g. `[:users, :likes]` # @return [Array] def precount(*association_names) # Allow single record instances as well as relations to be passed. # The splat here will return an array of the single record if it's # not a relation or otherwise return the records themselves via #to_a. records = *@relation return records if records.empty? # We need to get the relation's active class, which is the class itself # in the case of a single record. klass = @relation.respond_to?(:klass) ? @relation.klass : @relation.class association_names.each do |association_name| association_name = association_name.to_s reflection = klass.reflections.fetch(association_name) if reflection.inverse_of.nil? raise MissingInverseOf.new( "`#{reflection.klass}` does not have inverse of `#{klass}##{reflection.name}`. "\ "Probably missing to call `#{reflection.klass}.belongs_to #{klass.name.underscore.to_sym.inspect}`?" ) end primary_key = reflection.inverse_of.association_primary_key.to_sym count_by_id = if reflection.has_scope? # ActiveRecord 5.0 unscopes #scope_for argument, so adding #where outside that: # https://github.com/rails/rails/blob/v5.0.7/activerecord/lib/active_record/reflection.rb#L314-L316 reflection.scope_for(reflection.klass.unscoped).where(reflection.inverse_of.name => records.map(&primary_key)).group( reflection.inverse_of.foreign_key ).count else reflection.klass.where(reflection.inverse_of.name => records.map(&primary_key)).group( reflection.inverse_of.foreign_key ).count end writer = define_count_accessor(klass, association_name) records.each do |record| record.public_send(writer, count_by_id.fetch(record.public_send(primary_key), 0)) end end records end private # @param [Class] record class # @param [String] association_name # @return [String] writer method name def define_count_accessor(klass, association_name) reader_name = "#{association_name}_count" writer_name = "#{reader_name}=" if !klass.method_defined?(reader_name) && !klass.method_defined?(writer_name) klass.extend(ActiveRecord::Precountable) klass.public_send(:precounts, association_name) end writer_name end end end activerecord-precounter-0.4.0/lib/active_record/precountable.rb0000644000175100017510000000104414125330214025114 0ustar vivekdebvivekdebmodule ActiveRecord module Precountable class NotPrecountedError < StandardError end def precounts(*association_names) association_names.each do |association_name| var_name = "#{association_name}_count" instance_var_name = "@#{var_name}" attr_writer(var_name) define_method(var_name) do count = instance_variable_get(instance_var_name) raise NotPrecountedError.new("`#{association_name}' not precounted") unless count count end end end end end activerecord-precounter-0.4.0/ci/0000755000175100017510000000000014125330214017121 5ustar vivekdebvivekdebactiverecord-precounter-0.4.0/ci/travis.rb0000755000175100017510000000061014125330214020756 0ustar vivekdebvivekdeb#!/usr/bin/env ruby commands = [ 'mysql -e "create database activerecord_unittest;"', 'mysql -e "create database activerecord_unittest2;"', 'psql -c "create database activerecord_unittest;" -U postgres', 'psql -c "create database activerecord_unittest2;" -U postgres' ] commands.each do |command| system("#{command} > /dev/null 2>&1") end exit system("bundle exec rake $TASK") activerecord-precounter-0.4.0/ci/Gemfile.activerecord-5.2.x0000644000175100017510000000011214125330214023627 0ustar vivekdebvivekdebsource "https://rubygems.org" gem 'rails', '~> 5.2.0' gemspec path: '..' activerecord-precounter-0.4.0/ci/Gemfile.activerecord-5.1.x0000644000175100017510000000011214125330214023626 0ustar vivekdebvivekdebsource "https://rubygems.org" gem 'rails', '~> 5.1.0' gemspec path: '..' activerecord-precounter-0.4.0/ci/Gemfile.activerecord-5.0.x0000644000175100017510000000011214125330214023625 0ustar vivekdebvivekdebsource "https://rubygems.org" gem 'rails', '~> 5.0.0' gemspec path: '..' activerecord-precounter-0.4.0/ci/Gemfile.activerecord-4.2.x0000644000175100017510000000011214125330214023626 0ustar vivekdebvivekdebsource "https://rubygems.org" gem 'rails', '~> 4.2.0' gemspec path: '..' activerecord-precounter-0.4.0/bin/0000755000175100017510000000000014125330214017276 5ustar vivekdebvivekdebactiverecord-precounter-0.4.0/bin/setup0000755000175100017510000000020314125330214020357 0ustar vivekdebvivekdeb#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here activerecord-precounter-0.4.0/bin/console0000755000175100017510000000054614125330214020673 0ustar vivekdebvivekdeb#!/usr/bin/env ruby require "bundler/setup" require "activerecord/precounter" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) activerecord-precounter-0.4.0/activerecord-precounter.gemspec0000644000175100017510000000226714125330214024740 0ustar vivekdebvivekdeb# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'active_record/precounter/version' Gem::Specification.new do |spec| spec.name = 'activerecord-precounter' spec.version = ActiveRecord::Precounter::VERSION spec.authors = ['Takashi Kokubun'] spec.email = ['takashikkbn@gmail.com'] spec.summary = %q{Yet Another N+1 COUNT Query Killer for ActiveRecord} spec.description = %q{Yet Another N+1 COUNT Query Killer for ActiveRecord} spec.homepage = 'https://github.com/k0kubun/activerecord-precounter' spec.license = 'MIT' spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] spec.add_dependency 'activerecord', '>= 5' spec.add_development_dependency 'bundler' spec.add_development_dependency 'mysql2' spec.add_development_dependency 'pg' spec.add_development_dependency 'rake' spec.add_development_dependency 'rspec' spec.add_development_dependency 'sqlite3' end activerecord-precounter-0.4.0/Rakefile0000644000175100017510000000016514125330214020175 0ustar vivekdebvivekdebrequire "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task :default => :spec activerecord-precounter-0.4.0/README.md0000644000175100017510000000462114125330214020010 0ustar vivekdebvivekdeb# ActiveRecord::Precounter [![Build Status](https://travis-ci.org/k0kubun/activerecord-precounter.svg?branch=master)](https://travis-ci.org/k0kubun/activerecord-precounter) Yet Another N+1 COUNT Query Killer for ActiveRecord, counter\_cache alternative. ActiveRecord::Precounter allows you to cache count of associated records by eager loading. This is another version of [activerecord-precount](https://github.com/k0kubun/activerecord-precount), which is not elegant but designed to have no monkey-patch to ActiveRecord internal APIs for maintainability. ## Synopsis ### N+1 count query Sometimes you may see many count queries for one association. You can use counter\_cache to solve it, but you need to ALTER table and concern about dead lock to use counter\_cache. ```rb tweets = Tweet.all tweets.each do |tweet| p tweet.favorites.count end # SELECT `tweets`.* FROM `tweets` # SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 1 # SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 2 # SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 3 # SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 4 # SELECT COUNT(*) FROM `favorites` WHERE `favorites`.`tweet_id` = 5 ``` ### Count eager loading #### precount With activerecord-precounter gem installed, you can use `ActiveRecord::Precounter#precount` method to eagerly load counts of associated records. Like `preload`, it loads counts by multiple queries ```rb tweets = Tweet.all ActiveRecord::Precounter.new(tweets).precount(:favorites) tweets.each do |tweet| p tweet.favorites_count end # SELECT `tweets`.* FROM `tweets` # SELECT COUNT(`favorites`.`tweet_id`), `favorites`.`tweet_id` FROM `favorites` WHERE `favorites`.`tweet_id` IN (1, 2, 3, 4, 5) GROUP BY `favorites`.`tweet_id` ``` ## Installation Add this line to your application's Gemfile: ```ruby gem 'activerecord-precounter' ``` ## Limitation Target `has_many` association must have inversed `belongs_to`. i.e. `ActiveRecord::Precounter.new(tweets).precount(:favorites)` needs both `Tweet.has_many(:favorites)` and `Favorite.belongs_to(:tweet)`. Unlike [activerecord-precount](https://github.com/k0kubun/activerecord-precount), the cache store is not ActiveRecord association and it does not utilize ActiveRecord preloader. Thus you can't use `preload` to eager load counts for nested associations. And currently there's no JOIN support. ## License MIT License activerecord-precounter-0.4.0/LICENSE.txt0000644000175100017510000000207214125330214020352 0ustar vivekdebvivekdebThe MIT License (MIT) Copyright (c) 2017 Takashi Kokubun Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. activerecord-precounter-0.4.0/Gemfile0000644000175100017510000000023214125330214020016 0ustar vivekdebvivekdebsource 'https://rubygems.org' # Specify your gem's dependencies in activerecord-precounter.gemspec gemspec group :development, :test do gem 'pry' end activerecord-precounter-0.4.0/CHANGELOG.md0000644000175100017510000000225714125330214020345 0ustar vivekdebvivekdeb## v0.4.0 * [breaking] Raise `NotPrecountedError` instead of returning nil if `*_count` is called without precount * Add `ActiveRecord::Precountable` module to actively define `*_count` readers and writers by `precounts` DSL on ActiveRecord::Base. ## v0.3.3 * Support passing a single record to `ActiveRecord::Precounter.new` [#8](https://github.com/k0kubun/activerecord-precounter/pull/8) ## v0.3.2 * Support primary\_key option of belongs\_to association [#7](https://github.com/k0kubun/activerecord-precounter/pull/7) ## v0.3.1 * Fix [#5](https://github.com/k0kubun/activerecord-precounter/pull/5) for ActiveRecord 5.0 (take 3) ## v0.3.0 * Fix [#5](https://github.com/k0kubun/activerecord-precounter/pull/5) for ActiveRecord 5.0 (take 2) * [breaking] Drop ActiveRecord 4.2 support ## v0.2.3 * Fix count of scoped association [#5](https://github.com/k0kubun/activerecord-precounter/pull/5) ## v0.2.2 * Fix unexpected subquery [#4](https://github.com/k0kubun/activerecord-precounter/pull/5) ## v0.2.1 * Add support for counting scoped association * Return empty array when target records don't exist ## v0.2.0 * [breaking] Completely change interface ## v0.1.0 * Initial release activerecord-precounter-0.4.0/.travis.yml0000644000175100017510000000101714125330214020636 0ustar vivekdebvivekdebscript: ci/travis.rb language: ruby sudo: false cache: bundler branches: only: - master dist: trusty matrix: include: - rvm: 2.4.1 env: TASK=spec ARCONN=postgresql gemfile: ci/Gemfile.activerecord-5.0.x - rvm: 2.4.1 env: TASK=spec ARCONN=sqlite3 gemfile: ci/Gemfile.activerecord-5.1.x - rvm: 2.4.1 env: TASK=spec ARCONN=postgresql gemfile: ci/Gemfile.activerecord-5.1.x - rvm: 2.4.1 env: TASK=spec ARCONN=postgresql gemfile: ci/Gemfile.activerecord-5.2.x activerecord-precounter-0.4.0/.rspec0000644000175100017510000000003614125330214017642 0ustar vivekdebvivekdeb--color --require spec_helper activerecord-precounter-0.4.0/.gitignore0000644000175100017510000000021114125330214020510 0ustar vivekdebvivekdeb/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ # rspec failure tracking .rspec_status *.sqlite3