From a0e7e12b200f21407fc644cfc9c3c2f56880fe41 Mon Sep 17 00:00:00 2001 From: Ernie Miller Date: Mon, 5 Dec 2011 20:14:43 -0500 Subject: [PATCH] Add select_column/select_columns to AR::Base/AR::Relation --- activerecord/CHANGELOG.md | 26 ++++--- .../associations/collection_association.rb | 3 +- .../associations/collection_proxy.rb | 2 +- activerecord/lib/active_record/querying.rb | 68 ++++++++++++++++++- .../active_record/relation/calculations.rb | 56 ++++++++++++--- activerecord/test/cases/calculations_test.rb | 50 +++++++++++--- .../source/active_record_querying.textile | 12 ++-- 7 files changed, 178 insertions(+), 39 deletions(-) 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