root/trunk/activerecord/lib/active_record/calculations.rb
| Revision 9243, 13.9 kB (checked in by rick, 8 months ago) |
|---|
| Line | |
|---|---|
| 1 | module ActiveRecord |
| 2 | module Calculations #:nodoc: |
| 3 | CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include] |
| 4 | def self.included(base) |
| 5 | base.extend(ClassMethods) |
| 6 | end |
| 7 | |
| 8 | module ClassMethods |
| 9 | # Count operates using three different approaches. |
| 10 | # |
| 11 | # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. |
| 12 | # * Count using column : By passing a column name to count, it will return a count of all the rows for the model with supplied column present |
| 13 | # * Count using options will find the row count matched by the options used. |
| 14 | # |
| 15 | # The third approach, count using options, accepts an option hash as the only parameter. The options are: |
| 16 | # |
| 17 | # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro. |
| 18 | # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed) |
| 19 | # or named associations in the same form used for the :include option, which will perform an INNER JOIN on the associated table(s). |
| 20 | # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. |
| 21 | # Pass :readonly => false to override. |
| 22 | # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer |
| 23 | # to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting. |
| 24 | # See eager loading under Associations. |
| 25 | # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). |
| 26 | # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. |
| 27 | # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not |
| 28 | # include the joined columns. |
| 29 | # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... |
| 30 | # |
| 31 | # Examples for counting all: |
| 32 | # Person.count # returns the total count of all people |
| 33 | # |
| 34 | # Examples for counting by column: |
| 35 | # Person.count(:age) # returns the total count of all people whose age is present in database |
| 36 | # |
| 37 | # Examples for count with options: |
| 38 | # Person.count(:conditions => "age > 26") |
| 39 | # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. |
| 40 | # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins. |
| 41 | # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) |
| 42 | # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') |
| 43 | # |
| 44 | # Note: Person.count(:all) will not work because it will use :all as the condition. Use Person.count instead. |
| 45 | def count(*args) |
| 46 | calculate(:count, *construct_count_options_from_args(*args)) |
| 47 | end |
| 48 | |
| 49 | # Calculates the average value on a given column. The value is returned as a float. See #calculate for examples with options. |
| 50 | # |
| 51 | # Person.average('age') |
| 52 | def average(column_name, options = {}) |
| 53 | calculate(:avg, column_name, options) |
| 54 | end |
| 55 | |
| 56 | # Calculates the minimum value on a given column. The value is returned with the same data type of the column. See #calculate for examples with options. |
| 57 | # |
| 58 | # Person.minimum('age') |
| 59 | def minimum(column_name, options = {}) |
| 60 | calculate(:min, column_name, options) |
| 61 | end |
| 62 | |
| 63 | # Calculates the maximum value on a given column. The value is returned with the same data type of the column. See #calculate for examples with options. |
| 64 | # |
| 65 | # Person.maximum('age') |
| 66 | def maximum(column_name, options = {}) |
| 67 | calculate(:max, column_name, options) |
| 68 | end |
| 69 | |
| 70 | # Calculates the sum of values on a given column. The value is returned with the same data type of the column. See #calculate for examples with options. |
| 71 | # |
| 72 | # Person.sum('age') |
| 73 | def sum(column_name, options = {}) |
| 74 | calculate(:sum, column_name, options) || 0 |
| 75 | end |
| 76 | |
| 77 | # This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts. |
| 78 | # Options such as :conditions, :order, :group, :having, and :joins can be passed to customize the query. |
| 79 | # |
| 80 | # There are two basic forms of output: |
| 81 | # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else. |
| 82 | # * Grouped values: This returns an ordered hash of the values and groups them by the :group option. It takes either a column name, or the name |
| 83 | # of a belongs_to association. |
| 84 | # |
| 85 | # values = Person.maximum(:age, :group => 'last_name') |
| 86 | # puts values["Drake"] |
| 87 | # => 43 |
| 88 | # |
| 89 | # drake = Family.find_by_last_name('Drake') |
| 90 | # values = Person.maximum(:age, :group => :family) # Person belongs_to :family |
| 91 | # puts values[drake] |
| 92 | # => 43 |
| 93 | # |
| 94 | # values.each do |family, max_age| |
| 95 | # ... |
| 96 | # end |
| 97 | # |
| 98 | # Options: |
| 99 | # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro. |
| 100 | # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. |
| 101 | # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). |
| 102 | # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. |
| 103 | # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). |
| 104 | # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. |
| 105 | # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not |
| 106 | # include the joined columns. |
| 107 | # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... |
| 108 | # |
| 109 | # Examples: |
| 110 | # Person.calculate(:count, :all) # The same as Person.count |
| 111 | # Person.average(:age) # SELECT AVG(age) FROM people... |
| 112 | # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake' |
| 113 | # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors |
| 114 | # Person.sum("2 * age") |
| 115 | def calculate(operation, column_name, options = {}) |
| 116 | validate_calculation_options(operation, options) |
| 117 | column_name = options[:select] if options[:select] |
| 118 | column_name = '*' if column_name == :all |
| 119 | column = column_for column_name |
| 120 | catch :invalid_query do |
| 121 | if options[:group] |
| 122 | return execute_grouped_calculation(operation, column_name, column, options) |
| 123 | else |
| 124 | return execute_simple_calculation(operation, column_name, column, options) |
| 125 | end |
| 126 | end |
| 127 | 0 |
| 128 | end |
| 129 | |
| 130 | protected |
| 131 | def construct_count_options_from_args(*args) |
| 132 | options = {} |
| 133 | column_name = :all |
| 134 | |
| 135 | # We need to handle |
| 136 | # count() |
| 137 | # count(:column_name=:all) |
| 138 | # count(options={}) |
| 139 | # count(column_name=:all, options={}) |
| 140 | case args.size |
| 141 | when 1 |
| 142 | args[0].is_a?(Hash) ? options = args[0] : column_name = args[0] |
| 143 | when 2 |
| 144 | column_name, options = args |
| 145 | else |
| 146 | raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" |
| 147 | end if args.size > 0 |
| 148 | |
| 149 | [column_name, options] |
| 150 | end |
| 151 | |
| 152 | def construct_calculation_sql(operation, column_name, options) #:nodoc: |
| 153 | operation = operation.to_s.downcase |
| 154 | options = options.symbolize_keys |
| 155 | |
| 156 | scope = scope(:find) |
| 157 | merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) |
| 158 | aggregate_alias = column_alias_for(operation, column_name) |
| 159 | column_name = "#{connection.quote_table_name(table_name)}.#{column_name}" if column_names.include?(column_name.to_s) |
| 160 | |
| 161 | if operation == 'count' |
| 162 | if merged_includes.any? |
| 163 | options[:distinct] = true |
| 164 | column_name = options[:select] || [connection.quote_table_name(table_name), primary_key] * '.' |
| 165 | end |
| 166 | |
| 167 | if options[:distinct] |
| 168 | use_workaround = !connection.supports_count_distinct? |
| 169 | end |
| 170 | end |
| 171 | |
| 172 | if options[:distinct] && column_name.to_s !~ /\s*DISTINCT\s+/i |
| 173 | distinct = 'DISTINCT ' |
| 174 | end |
| 175 | sql = "SELECT #{operation}(#{distinct}#{column_name}) AS #{aggregate_alias}" |
| 176 | |
| 177 | # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. |
| 178 | sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround |
| 179 | |
| 180 | sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] |
| 181 | sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround |
| 182 | sql << " FROM #{connection.quote_table_name(table_name)} " |
| 183 | if merged_includes.any? |
| 184 | join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins]) |
| 185 | sql << join_dependency.join_associations.collect{|join| join.association_join }.join |
| 186 | end |
| 187 | add_joins!(sql, options, scope) |
| 188 | add_conditions!(sql, options[:conditions], scope) |
| 189 | add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) |
| 190 | |
| 191 | if options[:group] |
| 192 | group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field |
| 193 | sql << " GROUP BY #{options[group_key]} " |
| 194 | end |
| 195 | |
| 196 | if options[:group] && options[:having] |
| 197 | # FrontBase requires identifiers in the HAVING clause and chokes on function calls |
| 198 | if connection.adapter_name == 'FrontBase' |
| 199 | options[:having].downcase! |
| 200 | options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) |
| 201 | end |
| 202 | |
| 203 | sql << " HAVING #{options[:having]} " |
| 204 | end |
| 205 | |
| 206 | sql << " ORDER BY #{options[:order]} " if options[:order] |
| 207 | add_limit!(sql, options, scope) |
| 208 | sql << ')' if use_workaround |
| 209 | sql |
| 210 | end |
| 211 | |
| 212 | def execute_simple_calculation(operation, column_name, column, options) #:nodoc: |
| 213 | value = connection.select_value(construct_calculation_sql(operation, column_name, options)) |
| 214 | type_cast_calculated_value(value, column, operation) |
| 215 | end |
| 216 | |
| 217 | def execute_grouped_calculation(operation, column_name, column, options) #:nodoc: |
| 218 | group_attr = options[:group].to_s |
| 219 | association = reflect_on_association(group_attr.to_sym) |
| 220 | associated = association && association.macro == :belongs_to # only count belongs_to associations |
| 221 | group_field = associated ? association.primary_key_name : group_attr |
| 222 | group_alias = column_alias_for(group_field) |
| 223 | group_column = column_for group_field |
| 224 | sql = construct_calculation_sql(operation, column_name, options.merge(:group_field => group_field, :group_alias => group_alias)) |
| 225 | calculated_data = connection.select_all(sql) |
| 226 | aggregate_alias = column_alias_for(operation, column_name) |
| 227 | |
| 228 | if association |
| 229 | key_ids = calculated_data.collect { |row| row[group_alias] } |
| 230 | key_records = association.klass.base_class.find(key_ids) |
| 231 | key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } |
| 232 | end |
| 233 | |
| 234 | calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| |
| 235 | key = type_cast_calculated_value(row[group_alias], group_column) |
| 236 | key = key_records[key] if associated |
| 237 | value = row[aggregate_alias] |
| 238 | all[key] = type_cast_calculated_value(value, column, operation) |
| 239 | all |
| 240 | end |
| 241 | end |
| 242 | |
| 243 | private |
| 244 | def validate_calculation_options(operation, options = {}) |
| 245 | options.assert_valid_keys(CALCULATIONS_OPTIONS) |
| 246 | end |
| 247 | |
| 248 | # Converts a given key to the value that the database adapter returns as |
| 249 | # a usable column name. |
| 250 | # users.id #=> users_id |
| 251 | # sum(id) #=> sum_id |
| 252 | # count(distinct users.id) #=> count_distinct_users_id |
| 253 | # count(*) #=> count_all |
| 254 | def column_alias_for(*keys) |
| 255 | connection.table_alias_for(keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_')) |
| 256 | end |
| 257 | |
| 258 | def column_for(field) |
| 259 | field_name = field.to_s.split('.').last |
| 260 | columns.detect { |c| c.name.to_s == field_name } |
| 261 | end |
| 262 | |
| 263 | def type_cast_calculated_value(value, column, operation = nil) |
| 264 | operation = operation.to_s.downcase |
| 265 | case operation |
| 266 | when 'count' then value.to_i |
| 267 | when 'avg' then value && value.to_f |
| 268 | else column ? column.type_cast(value) : value |
| 269 | end |
| 270 | end |
| 271 | end |
| 272 | end |
| 273 | end |
Note: See TracBrowser for help on using the browser.