From 2bdfe3b3373ab3b95258f0a4672ddf542824252d Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Tue, 6 Dec 2011 08:18:07 -0500 Subject: [PATCH] Use relations in SelfGrouping. --- CHANGELOG | 9 +++ README.md | 32 ++++++++- .../arish/associations/association_scope.rb | 4 +- lib/grouped_scope/self_grouping.rb | 65 ++++++++++++++----- test/grouped_scope/self_grouping_test.rb | 54 +++++++++++++-- test/helper.rb | 1 + 6 files changed, 138 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5457776..9871a07 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ = master += 3.1.0 (unreleased) + +* Works with ActiveRecord 3.1 + +* The group object is now an ActiveRecord::Relation so you can further scope it. + +* New group.ids_sql which is an Arel SQL literal. Avoids large groups IDs and better query plans. + + = 0.6.0 (May 06, 2009) * ActiveRecord 2.3.14 compatibility. diff --git a/README.md b/README.md index 7d42473..1b6529d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ http://metaskills.net/2008/09/28/jack-has_many-things/ -## Installation & Usage +## Installation Install the gem with bundler. We follow a semantic versioning format that tracks ActiveRecord's minor version. So this means to use the latest 3.1.x version of GroupedScope with any ActiveRecord 3.1 version. @@ -17,6 +17,9 @@ Install the gem with bundler. We follow a semantic versioning format that tracks gem 'grouped_scope', '~> 3.1.0' ``` + +## Setup + To use GroupedScope on a model it must have a `:group_id` column. ```ruby @@ -30,6 +33,9 @@ class AddGroupId < ActiveRecord::Migration end ``` + +## General Usage + Assume the following model. ```ruby @@ -79,6 +85,30 @@ defined on the original association. For instance: ``` +## Advanced Usage + +The object returned by the `#group` method is an ActiveRecord relation on the targets class, +in this case `Employee`. Given this, you can further scope the grouped proxy if needed. Below, +we use the `:email_present` scope to refine the group down. + +```ruby +class Employee < ActiveRecord::Base + has_many :reports + grouped_scope :reports + scope :email_present, where("email IS NOT NULL") +end + +@employee_one = Employee.create :group_id => 5, :name => 'Ken' +@employee_two = Employee.create :group_id => 5, :name => 'MetaSkills', :email => 'ken@metaskills.net' + +# Only one employee is returned now. +@employee_one.group.email_present # => [# :proxy_class + delegate :quote_value, :columns_hash, :to => :proxy_class [].methods.each do |m| - unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|respond_to\?)/ - delegate m, :to => :group_proxy + unless m =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ + delegate m, :to => :grouped_proxy end end @@ -18,22 +18,50 @@ def initialize(proxy_owner) end def ids - @ids ||= find_selves(group_id_scope_options).map(&:id) + grouped_scoped_ids.map(&primary_key.to_sym) end + def ids_sql + Arel.sql(grouped_scoped_ids.to_sql) + end + + # TODO: Note this. def quoted_ids ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',') end + def with_reflection(reflection) + @reflection = reflection + yield + ensure + @reflection = nil + end + def respond_to?(method, include_private=false) - super || proxy_class.grouped_reflections[method].present? + super || proxy_class.grouped_reflections[method].present? || grouped_proxy.respond_to?(method, include_private) end protected - def group_proxy - @group_proxy ||= find_selves(group_scope_options) + def primary_key + reflection ? reflection.association_primary_key : proxy_class.primary_key + end + + def arel_group_id + arel_table['group_id'] + end + + def arel_primary_key + arel_table[primary_key] + end + + def arel_table + reflection ? Arel::Table.new(reflection.table_name) : proxy_class.arel_table + end + + def grouped_proxy + @grouped_proxy ||= grouped_scoped end def grouped? @@ -44,18 +72,13 @@ def all_grouped? proxy_owner.all_grouped? rescue false end - def find_selves(options={}) - proxy_owner.class.find :all, options - end - - def group_scope_options - return {} if all_grouped? - conditions = grouped? ? { :group_id => proxy_owner.group_id } : { primary_key => proxy_owner.id } - { :conditions => conditions } + def grouped_scoped + return proxy_class.scoped if all_grouped? + proxy_class.where grouped? ? arel_group_id.eq(proxy_owner.group_id) : arel_primary_key.eq(proxy_owner.id) end - def group_id_scope_options - { :select => primary_key }.merge(group_scope_options) + def grouped_scoped_ids + grouped_scoped.select(arel_primary_key) end def proxy_class @@ -72,6 +95,12 @@ def method_missing(method, *args) else proxy_owner.send(:"grouped_scope_#{method}", *args) end + elsif grouped_proxy.respond_to?(method) + if block_given? + grouped_proxy.send(method, *args) { |*block_args| yield(*block_args) } + else + grouped_proxy.send(method, *args) + end else super end diff --git a/test/grouped_scope/self_grouping_test.rb b/test/grouped_scope/self_grouping_test.rb index a4c9c01..b55539e 100644 --- a/test/grouped_scope/self_grouping_test.rb +++ b/test/grouped_scope/self_grouping_test.rb @@ -8,12 +8,11 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase @employee = FactoryGirl.create(:employee) end - it 'return an array' do - assert_instance_of Array, @employee.group - end - it 'return #ids array' do assert_equal [@employee.id], @employee.group.ids + e1 = FactoryGirl.create :employee, :group_id => 3 + e2 = FactoryGirl.create :employee, :group_id => 3 + assert_same_elements [e1.id, e2.id], e1.group.ids end it 'return #quoted_ids string for use in sql statments' do @@ -35,6 +34,28 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase end end + describe 'for #with_reflection' do + + before { @reflection = Employee.reflections[:reports] } + + it 'will set a reflection and always set it back to nil' do + assert_nil @employee.group.reflection + @employee.group.with_reflection(@reflection) do + assert_equal @reflection, @employee.group.reflection + end + assert_nil @employee.group.reflection + end + + it 'will use the primary key of the reflection' do + pk = 'association_primary_key' + @reflection.stubs :association_primary_key => pk + @employee.group.with_reflection(@reflection) do + assert_equal pk, @employee.group.send(:primary_key) + end + end + + end + describe 'for Array delegates' do it 'respond to first/last' do @@ -56,8 +77,8 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase describe 'Calling #group' do - it 'return an array' do - assert_instance_of Array, FactoryGirl.create(:employee).group + it 'returns a active record relation' do + assert_instance_of ActiveRecord::Relation, FactoryGirl.create(:employee).group end describe 'with a NIL group_id' do @@ -74,6 +95,11 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase assert @employee.group.include?(@employee) end + it 'returns a sql literal for #ids_sql scoped to single record' do + @employee.group.ids_sql.must_be_instance_of Arel::Nodes::SqlLiteral + @employee.group.ids_sql.must_match %r{SELECT \"employees\".\"id\" FROM \"employees\" WHERE \"employees\".\"id\" = #{@employee.id}} + end + end describe 'with a set group_id' do @@ -90,6 +116,22 @@ class GroupedScope::SelfGrouppingTest < GroupedScope::TestCase assert @employee.group.include?(@employee) end + it 'returns a sql literal for #ids_sql scoped to group' do + new_group_id = 420 + e1 = FactoryGirl.create :employee, :group_id => new_group_id + e2 = FactoryGirl.create :employee, :group_id => new_group_id + e1.group.ids_sql.must_be_instance_of Arel::Nodes::SqlLiteral + e1.group.ids_sql.must_match %r{SELECT \"employees\".\"id\" FROM \"employees\" WHERE \"employees\".\"group_id\" = #{new_group_id}} + end + + it 'allows the group to be further scoped' do + new_group_id = 420 + e1 = FactoryGirl.create :employee, :group_id => new_group_id, :name => 'Ken', :email => 'ken@actionmoniker.com' + e2 = FactoryGirl.create :employee, :group_id => new_group_id, :name => 'Hostmaster', :email => 'hostmaster@actionmoniker.com' + e3 = FactoryGirl.create :employee, :group_id => new_group_id, :name => 'Ken', :email => 'ken@metaskills.net' + assert_same_elements [e1, e2], e1.group.email_for_actionmoniker + end + end describe 'with different groups available' do diff --git a/test/helper.rb b/test/helper.rb index 42b7621..d2a64e8 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -147,6 +147,7 @@ class LegacyReport < ActiveRecord::Base end class Employee < ActiveRecord::Base + scope :email_for_actionmoniker, where("email LIKE '%@actionmoniker.com'") has_many :reports do ; def urgent ; find(:all,:conditions => {:title => 'URGENT'}) ; end ; end has_many :taxonomies, :as => :classable has_many :department_memberships