Permalink
Browse files

Use relations in SelfGrouping.

  • Loading branch information...
1 parent 311ad5b commit 2bdfe3b3373ab3b95258f0a4672ddf542824252d @metaskills committed Dec 6, 2011
View
@@ -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.
View
@@ -9,14 +9,17 @@ 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.
```ruby
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 # => [#<Employee id: 1, group_id: 5, name: 'MetaSkills', email: 'ken@metaskills.net']
+```
+
+
+
+
+
## Todo List
@@ -10,8 +10,8 @@ class AssociationScope < ActiveRecord::Associations::AssociationScope
# in chunks, it would be easier to hook into. This more elegant version which supers
# up will only work for the has_many. https://gist.github.com/1434980
#
- # We will just have to monitor rails everynow and then and update this. Thankfully this
- # copy is only used in a group scope. FYI, our one line change is below.
+ # We will just have to monitor rails every now and then and update this. Thankfully this
+ # copy is only used in a group scope. FYI, our one line change is commented below.
def add_constraints(scope)
tables = construct_tables
@@ -1,13 +1,13 @@
module GroupedScope
class SelfGroupping
- attr_reader :proxy_owner
+ attr_reader :proxy_owner, :reflection
- delegate :primary_key, :quote_value, :columns_hash, :to => :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
@@ -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
View
@@ -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

0 comments on commit 2bdfe3b

Please sign in to comment.