From 5f0e96b09635ca7e15edb2ab4f1a7cbed9700da0 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Thu, 2 May 2013 02:57:46 -0400 Subject: [PATCH] Add `group_by` to `ActiveRecord::FinderMethods` Enables collecting of records into sets, grouped by distinct values for the specified `field`. Leverages `ActiveRecord::Relation` to be far more efficient than `Enumerable#group_by` when selecting based on a column name. Example: User.group_by('role') # => { # "normal" => #, # "admin" => # # } --- activerecord/CHANGELOG.md | 12 ++++++++ .../active_record/relation/finder_methods.rb | 21 ++++++++++++++ activerecord/test/cases/finder_test.rb | 29 +++++++++++++++++++ guides/source/active_record_querying.md | 25 ++++++++++++++-- 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index e874486b537fc..3b20927503129 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,15 @@ +* Add `ActiveRecord::FinderMethods#group_by` method, which collects records + into sets, grouped by distinct values for the specified `field`. This patch + leverages `ActiveRecord::Relation` to be far more efficient than + `Enumerable#group_by` when selecting based on a column name. + + Example: + + User.group_by(:role) + # => {"normal" => #, "admin" => #} + + *Aidan Feldman* + * Since the `test_help.rb` in Railties now automatically maintains your test schema, the `rake db:test:*` tasks are deprecated. This doesn't stop you manually running other tasks on your test database diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3963f2b3e0678..419a024d4b7eb 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -217,6 +217,27 @@ def exists?(conditions = :none) connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false end + # Collect records into sets, grouped by distinct values for the specified +field+. + # + # User.select([:id, :name]) + # => [#, #, # + # + # User.group_by(:name) + # => {"Foo" => #, "Oscar" => #} + def group_by(field = nil, &block) + if field.nil? || block_given? + super(&block) + else + result = {} + select(field).distinct.each do |item| + value = item[field] + result[value] = where(field => value) + end + + result + end + end + # This method is called whenever no records are found with either a single # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception. # diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 5125d5df2a5f1..8ca09ce7d1716 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -883,6 +883,35 @@ def test_finder_with_offset_string assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a } end + def test_group_by_with_no_args + assert_kind_of Enumerator, Company.all.group_by + end + + def test_group_by_with_symbol + groups = Company.all.group_by(:type) + assert_kind_of Hash, groups + + keys = groups.keys + assert_equal 5, keys.size + assert_equal [nil, 'Client', 'DependentFirm', 'ExclusivelyDependentFirm', 'Firm'].to_set, keys.to_set + + assert_equal [2, 3, 5, 10], groups['Client'].map(&:id).sort + assert_equal [1, 4], groups['Firm'].map(&:id).sort + assert_equal nil, groups['Nonexistent'] + end + + def test_group_by_with_block + groups = Company.all.group_by { |c| c.firm_id } + + group = groups[4] + assert_kind_of Array, group + assert_equal [5, 10], group.map(&:id).sort + end + + def test_group_by_with_no_args + assert_kind_of Enumerable, Company.all.group_by + end + protected def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 4725e2c8a21d7..e6bd9f8575710 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -638,8 +638,10 @@ will return instead a maximum of 5 clients beginning with the 31st. The SQL look SELECT * FROM clients LIMIT 5 OFFSET 30 ``` -Group ------ +Grouping +-------- + +### `group` To apply a `GROUP BY` clause to the SQL fired by the finder, you can specify the `group` method on the find. @@ -659,6 +661,25 @@ FROM orders GROUP BY date(created_at) ``` +### `group_by` + +Similar to [Ruby's `Enumerable#group_by`](http://ruby-doc.org/core/Enumerable.html#method-i-group_by), Active Record's `group_by` method provides a way to retrieve all records that correspond to each distinct value. + +For example, to get the list of users with each role: + +```ruby +Address.group_by(:state) +# => {"AL" => #, "AK" => #, ...} +``` + +Under the hood, it will execute the SQL to retrieve the distinct values of the column: + +```sql +# SELECT DISTINCT state FROM addresses +``` + +Without loading the full records from the database. + Having ------