diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 348873d7597b1..52ce7f3b7a69f 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,5 +1,21 @@
## Rails 3.2.0 (unreleased) ##
+* Added `ActiveRecord::Base#select_column` and `ActiveRecord::Base#select_columns`
+
+ Returns an array (or an array of arrays, in the case of `select_columns`) of
+ type-cast values for a class or relation, without instantiating AR::Base
+ objects.
+
+ Client.where(:active => true).select_column(:id)
+
+ is the same as
+
+ Client.where(:active => true).select(:id).map(&:id)
+
+ but doesn't require instantiation of multiple Client objects.
+
+ *Ernie Miller*
+
* Added ability to run migrations only for given scope, which allows
to run migrations only from one engine (for example to revert changes
from engine that you want to remove).
@@ -27,14 +43,6 @@
*fxn*
-* Implemented ActiveRecord::Relation#pluck method
-
- Method returns Array of column value from table under ActiveRecord model
-
- Client.pluck(:id)
-
- *Bogdan Gusiev*
-
* Automatic closure of connections in threads is deprecated. For example
the following code is deprecated:
@@ -233,7 +241,7 @@
* LRU cache in mysql and sqlite are now per-process caches.
- * lib/active_record/connection_adapters/mysql_adapter.rb: LRU cache keys are per process id.
+ * lib/active_record/connection_adapters/mysql_adapter.rb: LRU cache keys are per process id.
* lib/active_record/connection_adapters/sqlite_adapter.rb: ditto
*Aaron Patterson*
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index fe9f30bd2a88b..7756450292be2 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -48,7 +48,6 @@ def ids_reader
record.send(reflection.association_primary_key)
end
else
- column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
relation = scoped
including = (relation.eager_load_values + relation.includes_values).uniq
@@ -60,7 +59,7 @@ def ids_reader
end
end
- relation.uniq.pluck(column)
+ relation.uniq.select_column(reflection.association_primary_key)
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index eb320bc774ea0..ea63484ce3758 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -39,7 +39,7 @@ class CollectionProxy # :nodoc:
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
- :lock, :readonly, :having, :pluck, :to => :scoped
+ :lock, :readonly, :having, :select_column, :select_columns, :to => :scoped
delegate :target, :load_target, :loaded?, :to => :@association
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 09da9ad1d1ee2..b363c424e7988 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -9,7 +9,7 @@ module Querying
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, :pluck, :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
# be returned as an array with columns requested encapsulated as attributes of the model you call
@@ -54,5 +54,71 @@ def count_by_sql(sql)
sql = sanitize_conditions(sql)
connection.select_value(sql, "#{name} Count").to_i
end
+
+ # Returns an Array containing the type-cast values of a single
+ # attribute of all records of this class. This is identical to the
+ # idiom:
+ #
+ # Person.select(:id).map(&:id)
+ #
+ # but without the overhead of instantiating each ActiveRecord::Base
+ # object.
+ #
+ # Examples:
+ #
+ # Person.select_column(:id) # SELECT people.id FROM people
+ def select_column(attr_name)
+ attr_name = attr_name.to_s
+ attr_name = primary_key if attr_name == 'id'
+
+ column = columns_hash[attr_name]
+ coder = serialized_attributes[attr_name]
+
+ connection.select_rows(
+ except(:select).select(arel_table[attr_name]).to_sql
+ ).map! do |values|
+ type_cast_for_select_column(values[0], column, coder)
+ end
+ end
+
+ # Returns an Array which contains an Array for each
+ # record of this class. Each internal array contains the type-cast
+ # values of the attributes given as parameters. Like select_column,
+ # this avoids the overhead of instantiating each ActiveRecord::Base
+ # object, but it also allows for the following syntax:
+ #
+ # Person.select_columns(:name, :email) do |name, email|
+ # puts "#{name}'s e-mail address is #{email}"
+ # end
+ def select_columns(*attr_names)
+ attr_names.map! do |attr_name|
+ attr_name = attr_name.to_s
+ attr_name == 'id' ? primary_key : attr_name
+ end
+
+ columns = attr_names.map {|n| columns_hash[n]}
+ coders = attr_names.map {|n| serialized_attributes[n]}
+
+ connection.select_rows(
+ except(:select).select(attr_names.map {|n| arel_table[n]}).to_sql
+ ).map! do |values|
+ values.each_with_index do |value, index|
+ values[index] = type_cast_for_select_column(value, columns[index], coders[index])
+ end
+ end
+ end
+
+ # Given a value, a column definition, and a coder, type-cast or
+ # decode the value.
+ def type_cast_for_select_column(value, column, coder)
+ if value.nil? || !column
+ value
+ elsif coder
+ coder.load(value)
+ else
+ column.type_cast(value)
+ end
+ end
+
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 0f57e9831db59..bd7fde5f9c211 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -166,20 +166,56 @@ def calculate(operation, column_name, options = {})
0
end
- # This method is designed to perform select by a single column as direct SQL query
- # Returns Array with values of the specified column name
- # The values has same data type as column.
+ # Returns an Array containing the type-cast values of a single
+ # attribute of all records retrieved by the relation. This is identical
+ # to the idiom:
+ #
+ # Person.where(:confirmed => true).select(:id).map(&:id)
+ #
+ # but without the overhead of instantiating each ActiveRecord::Base
+ # object in the relation.
#
# Examples:
#
- # Person.pluck(:id) # SELECT people.id FROM people
- # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people
- # Person.where(:confirmed => true).limit(5).pluck(:id)
+ # Person.uniq.select_column(:role) # SELECT DISTINCT role FROM people
+ # Person.where(:confirmed => true).limit(5).select_column(:id)
+ def select_column(attr_name)
+ attr_name = attr_name.to_s
+ attr_name = klass.primary_key if attr_name == 'id'
+
+ # Don't re-run query if the records have already been loaded.
+ if loaded? && (empty? || first.attributes.has_key?(attr_name))
+ to_a.map {|record| record[attr_name]}
+ else
+ scoping { klass.select_column attr_name }
+ end
+ end
+
+ # Returns an Array which contains an Array for each
+ # record retrieved by the relation. Each internal array contains the
+ # type-cast values of the attributes given as parameters. Like
+ # select_column, this avoids the overhead of instantiating each
+ # ActiveRecord::Base object in the relation, but it allows for the
+ # following:
#
- def pluck(column_name)
- scope = self.select(column_name)
- self.connection.select_values(scope.to_sql).map! do |value|
- type_cast_using_column(value, column_for(column_name))
+ # Person.where(:confirmed => true).select_columns(:name, :email) do |name, email|
+ # puts "#{name}'s e-mail address is #{email}"
+ # end
+ #
+ # Example:
+ #
+ # Person.where(:confirmed => true).limit(5).select_columns(:name, :salary)
+ def select_columns(*attr_names)
+ attr_names.map! do |attr_name|
+ attr_name = attr_name.to_s
+ attr_name == 'id' ? klass.primary_key : attr_name
+ end
+
+ # Don't re-run query if the records have already been loaded.
+ if loaded? && (empty? || attr_names.all? {|a| first.attributes.has_key? a})
+ to_a.map {|record| attr_names.map {|a| record[a]}}
+ else
+ scoping { klass.select_columns(*attr_names) }
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 5abf3d1af4afc..68227d4007654 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -448,27 +448,57 @@ def test_distinct_is_honored_when_used_with_count_operation_after_group
assert_equal distinct_authors_for_approved_count, 2
end
- def test_pluck
- assert_equal [1,2,3,4], Topic.order(:id).pluck(:id)
+ def test_select_column
+ assert_equal [1,2,3,4], Topic.order(:id).select_column(:id)
end
- def test_pluck_type_cast
+ def test_select_column_type_cast
topic = topics(:first)
relation = Topic.where(:id => topic.id)
- assert_equal [ topic.approved ], relation.pluck(:approved)
- assert_equal [ topic.last_read ], relation.pluck(:last_read)
- assert_equal [ topic.written_on ], relation.pluck(:written_on)
+ assert_equal [ topic.approved ], relation.select_column(:approved)
+ assert_equal [ topic.last_read ], relation.select_column(:last_read)
+ assert_equal [ topic.written_on ], relation.select_column(:written_on)
+ end
+
+ def test_select_column_and_uniq
+ assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.select_column(:credit_limit)
+ end
+ def test_select_column_runs_necessary_queries
+ topics = Topic.select(:title)
+ topics.all
+ assert_queries 1 do
+ assert_equal [1,2,3,4], topics.select_column(:id)
+ end
end
- def test_pluck_and_uniq
- assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.pluck(:credit_limit)
+ def test_select_column_does_not_run_unnecessary_queries
+ topics = Topic.scoped
+ topics.all
+ assert_queries 0 do
+ assert_equal [1,2,3,4], topics.select_column(:id)
+ end
end
- def test_pluck_in_relation
+ def test_select_column_on_association
company = Company.first
contract = company.contracts.create!
- assert_equal [contract.id], company.contracts.pluck(:id)
+ assert_equal [contract.id], company.contracts.select_column(:id)
+ end
+
+ def test_select_columns
+ assert_equal [
+ [1, 'The First Topic'],
+ [2, 'The Second Topic of the day'],
+ [3, 'The Third Topic of the day'],
+ [4, 'The Fourth Topic of the day'] ], Topic.order(:id).select_columns(:id, :title)
+ end
+
+ def test_select_columns_type_cast
+ topic = topics(:first)
+ relation = Topic.where(:id => topic.id)
+ assert_equal [ [topic.approved, topic.last_read, topic.written_on] ],
+ relation.select_columns(:approved, :last_read, :written_on)
end
end
diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile
index 0cbabd71a186c..2fe40820dd9e4 100644
--- a/railties/guides/source/active_record_querying.textile
+++ b/railties/guides/source/active_record_querying.textile
@@ -1146,19 +1146,19 @@ h3. +select_all+
Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
-h3. +pluck+
+h3. +select_column+
-pluck can be used to query a single column from the underlying table of a model. It accepts a column name as argument and returns an array of values of the specified column with the corresponding data type.
+select_column can be used to query a single column from the underlying table of a model. It accepts a column name as argument and returns an array of values of the specified column with the corresponding data type.
-Client.where(:active => true).pluck(:id)
+Client.where(:active => true).select_column(:id)
# SELECT id FROM clients WHERE active = 1
-Client.uniq.pluck(:role)
+Client.uniq.select_column(:role)
# SELECT DISTINCT role FROM clients
-+pluck+ makes it possible to replace code like
++select_column+ makes it possible to replace code like
Client.select(:id).map { |c| c.id }
@@ -1167,7 +1167,7 @@ Client.select(:id).map { |c| c.id }
with
-Client.pluck(:id)
+Client.select_column(:id)
h3. Existence of Objects