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 value_of/values_of to AR::Base/AR::Relation #3871

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
26 changes: 17 additions & 9 deletions 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).
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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*

Expand Down
Expand Up @@ -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
Expand All @@ -60,7 +59,7 @@ def ids_reader
end
end

relation.uniq.pluck(column)
relation.uniq.select_column(reflection.association_primary_key)
end
end

Expand Down
Expand Up @@ -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

Expand Down
68 changes: 67 additions & 1 deletion activerecord/lib/active_record/querying.rb
Expand Up @@ -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
Expand Down Expand Up @@ -54,5 +54,71 @@ def count_by_sql(sql)
sql = sanitize_conditions(sql)
connection.select_value(sql, "#{name} Count").to_i
end

# Returns an <tt>Array</tt> 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 <tt>Array</tt> which contains an <tt>Array</tt> for each
# record of this class. Each internal array contains the type-cast
# values of the attributes given as parameters. Like <tt>select_column</tt>,
# 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
56 changes: 46 additions & 10 deletions activerecord/lib/active_record/relation/calculations.rb
Expand Up @@ -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 <tt>Array</tt> with values of the specified column name
# The values has same data type as column.
# Returns an <tt>Array</tt> 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 <tt>Array</tt> which contains an <tt>Array</tt> for each
# record retrieved by the relation. Each internal array contains the
# type-cast values of the attributes given as parameters. Like
# <tt>select_column</tt>, 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

Expand Down
50 changes: 40 additions & 10 deletions activerecord/test/cases/calculations_test.rb
Expand Up @@ -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
12 changes: 6 additions & 6 deletions railties/guides/source/active_record_querying.textile
Expand Up @@ -1146,19 +1146,19 @@ h3. +select_all+
Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
</ruby>

h3. +pluck+
h3. +select_column+

<tt>pluck</tt> 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.
<tt>select_column</tt> 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.

<ruby>
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
</ruby>

+pluck+ makes it possible to replace code like
+select_column+ makes it possible to replace code like

<ruby>
Client.select(:id).map { |c| c.id }
Expand All @@ -1167,7 +1167,7 @@ Client.select(:id).map { |c| c.id }
with

<ruby>
Client.pluck(:id)
Client.select_column(:id)
</ruby>

h3. Existence of Objects
Expand Down