Skip to content

Commit

Permalink
Add ActiveRecord::Relation#uniq for toggling DISTINCT in the SQL query
Browse files Browse the repository at this point in the history
  • Loading branch information
jonleighton committed Nov 5, 2011
1 parent 6aaae3d commit 562583c
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 13 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ gemspec

if ENV['AREL']
gem "arel", :path => ENV['AREL']
else
gem "arel", :git => "git://github.com/rails/arel"
end

gem "bcrypt-ruby", "~> 3.0.0"
Expand Down
16 changes: 16 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
## Rails 3.2.0 (unreleased) ##

* Add ActiveRecord::Relation#uniq for generating unique queries.

Before:

Client.select('DISTINCT name')

After:

Client.select(:name).uniq

This also allows you to revert the unqueness in a relation:

This comment has been minimized.

Copy link
@findchris

findchris Dec 12, 2011

unqueness => uniqueness


Client.select(:name).uniq.uniq(false)

*Jon Leighton*

* Support index sort order in sqlite, mysql and postgres adapters. *Vlad Jebelev*

* Allow the :class_name option for associations to take a symbol (:Client) in addition to
Expand Down
4 changes: 3 additions & 1 deletion activerecord/lib/active_record/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,9 @@ class << self # Class methods
delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
:where, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :to => :scoped
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped

# Executes a custom SQL query against your database and returns all the results. The results will
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Relation
JoinOperation = Struct.new(:relation, :join_class, :on)
ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq]

include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches

Expand Down
30 changes: 24 additions & 6 deletions activerecord/lib/active_record/relation/query_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ module QueryMethods
:select_values, :group_values, :order_values, :joins_values,
:where_values, :having_values, :bind_values,
:limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value,
:from_value, :reorder_value, :reverse_order_value
:from_value, :reorder_value, :reverse_order_value,
:uniq_value

def includes(*args)
args.reject! {|a| a.blank? }
Expand Down Expand Up @@ -38,7 +39,7 @@ def preload(*args)
end

# Works in two unique ways.
#
#
# First: takes a block so it can be used just like Array#select.
#
# Model.scoped.select { |m| m.field == value }
Expand Down Expand Up @@ -176,9 +177,25 @@ def from(value)
relation
end

# Specifies whether the records should be unique or not. For example:
#
# User.select(:name)
# # => Might return two records with the same name
#
# User.select(:name).uniq
# # => Returns 1 record per unique name
#
# User.select(:name).uniq.uniq(false)
# # => You can also remove the uniqueness
def uniq(value = true)
relation = clone
relation.uniq_value = value
relation
end

# Used to extend a scope with additional methods, either through
# a module or through a block provided.
#
# a module or through a block provided.
#
# The object returned is a relation, which can be further extended.
#
# === Using a module
Expand All @@ -200,7 +217,7 @@ def from(value)
#
# scope = Model.scoped.extending do
# def page(number)
# # pagination code goes here
# # pagination code goes here
# end
# end
# scope.page(params[:page])
Expand All @@ -209,7 +226,7 @@ def from(value)
#
# scope = Model.scoped.extending(Pagination) do
# def per_page(number)
# # pagination code goes here
# # pagination code goes here
# end
# end
def extending(*modules)
Expand Down Expand Up @@ -252,6 +269,7 @@ def build_arel

build_select(arel, @select_values.uniq)

arel.distinct(@uniq_value)
arel.from(@from_value) if @from_value
arel.lock(@lock_value) if @lock_value

Expand Down
6 changes: 6 additions & 0 deletions activerecord/test/cases/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1935,4 +1935,10 @@ def test_cache_key_format_for_existing_record_with_nil_updated_at
dev.update_attribute(:updated_at, nil)
assert_match(/\/#{dev.id}$/, dev.cache_key)
end

def test_uniq_delegates_to_scoped
scope = stub
Bird.stubs(:scoped).returns(mock(:uniq => scope))
assert_equal scope, Bird.uniq
end
end
2 changes: 1 addition & 1 deletion activerecord/test/cases/relation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def test_construction
end

def test_single_values
assert_equal [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order].map(&:to_s).sort,
assert_equal [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq].map(&:to_s).sort,
Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort
end

Expand Down
16 changes: 16 additions & 0 deletions activerecord/test/cases/relations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1148,4 +1148,20 @@ def test_update_all_with_joins_and_offset_and_order
assert_equal posts(:thinking), comments(:more_greetings).post
assert_equal posts(:welcome), comments(:greetings).post
end

def test_uniq
tag1 = Tag.create(:name => 'Foo')
tag2 = Tag.create(:name => 'Foo')

query = Tag.select(:name).where(:id => [tag1.id, tag2.id])

assert_equal ['Foo', 'Foo'], query.map(&:name)
assert_sql(/DISTINCT/) do
assert_equal ['Foo'], query.uniq.map(&:name)
end
assert_sql(/DISTINCT/) do
assert_equal ['Foo'], query.uniq(true).map(&:name)
end
assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name)
end
end
24 changes: 20 additions & 4 deletions railties/guides/source/active_record_querying.textile
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ end

But this approach becomes increasingly impractical as the table size increases, since +User.all.each+ instructs Active Record to fetch _the entire table_ in a single pass, build a model object per row, and then keep the entire array of model objects in memory. Indeed, if we have a large number of records, the entire collection may exceed the amount of memory available.

Rails provides two methods that address this problem by dividing records into memory-friendly batches for processing. The first method, +find_each+, retrieves a batch of records and then yields _each_ record to the block individually as a model. The second method, +find_in_batches+, retrieves a batch of records and then yields _the entire batch_ to the block as an array of models.
Rails provides two methods that address this problem by dividing records into memory-friendly batches for processing. The first method, +find_each+, retrieves a batch of records and then yields _each_ record to the block individually as a model. The second method, +find_in_batches+, retrieves a batch of records and then yields _the entire batch_ to the block as an array of models.

TIP: The +find_each+ and +find_in_batches+ methods are intended for use in the batch processing of a large number of records that wouldn't fit in memory all at once. If you just need to loop over a thousand records the regular find methods are the preferred option.

Expand Down Expand Up @@ -435,10 +435,26 @@ ActiveModel::MissingAttributeError: missing attribute: <attribute>

Where +&lt;attribute&gt;+ is the attribute you asked for. The +id+ method will not raise the +ActiveRecord::MissingAttributeError+, so just be careful when working with associations because they need the +id+ method to function properly.

You can also call SQL functions within the select option. For example, if you would like to only grab a single record per unique value in a certain field by using the +DISTINCT+ function you can do it like this:
If you would like to only grab a single record per unique value in a certain field, you can use +uniq+:

<ruby>
Client.select("DISTINCT(name)")
Client.select(:name).uniq
</ruby>

This would generate SQL like:

<sql>
SELECT DISTINCT name FROM clients
</sql>

You can also remove the uniqueness constraint:

<ruby>
query = Client.select(:name).uniq
# => Returns unique names

query.uniq(false)
# => Returns all names, even if there are duplicates
</ruby>

h3. Limit and Offset
Expand Down Expand Up @@ -741,7 +757,7 @@ SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
</sql>

Or, in English: "return a Category object for all categories with posts". Note that you will see duplicate categories if more than one post has the same category. If you want unique categories, you can use Category.joins(:post).select("distinct(categories.id)").
Or, in English: "return a Category object for all categories with posts". Note that you will see duplicate categories if more than one post has the same category. If you want unique categories, you can use Category.joins(:post).select("distinct(categories.id)").

h5. Joining Multiple Associations

Expand Down

0 comments on commit 562583c

Please sign in to comment.