Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add group_by to ActiveRecord::FinderMethods, take two #10797

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions 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" => #<ActiveRecord::Relation [...]>, "admin" => #<ActiveRecord::Relation [...]>}

*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
Expand Down
21 changes: 21 additions & 0 deletions activerecord/lib/active_record/relation/finder_methods.rb
Expand Up @@ -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 id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">
#
# User.group_by(:name)
# => {"Foo" => #<ActiveRecord::Relation [...]>, "Oscar" => #<ActiveRecord::Relation [...]>}
def group_by(field = nil, &block)
if field.nil? || block_given?
super(&block)
else
result = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about using an HashWithIndifferentAccess here ?

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.
#
Expand Down
29 changes: 29 additions & 0 deletions activerecord/test/cases/finder_test.rb
Expand Up @@ -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)
Expand Down
25 changes: 23 additions & 2 deletions guides/source/active_record_querying.md
Expand Up @@ -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.

Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you wrap this line around 80 chars as well please ? 😄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the rest of the guide is wrapped, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but we are slowly wrapping the new additions around 80 chars. ;-) But let's wait some more feedback before updating anything.


For example, to get the list of users with each role:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not consistent with your example ; you should either update the example or this sentence. Actually, I would be a bit more descriptive, saying something like "Given you have a X model with a Y column, you can group ...", what do you think ?


```ruby
Address.group_by(:state)
# => {"AL" => #<ActiveRecord::Relation [...]>, "AK" => #<ActiveRecord::Relation [...]>, ...}
```

Under the hood, it will execute the SQL to retrieve the distinct values of the column:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"it will execute the following SQL query", what do you think ?


```sql
# SELECT DISTINCT state FROM addresses
```

Without loading the full records from the database.

Having
------

Expand Down