module VirtualAttributes::VirtualTotal::ClassMethods
Private Instance Methods
define_virtual_aggregate_attribute(name, relation, method_name, column, options)
click to toggle source
# File lib/active_record/virtual_attributes/virtual_total.rb, line 81 def define_virtual_aggregate_attribute(name, relation, method_name, column, options) reflection = reflect_on_association(relation) if options.key?(:arel) arel = options.dup.delete(:arel) # if there is no relation to get to the arel, have to throw it away arel = nil if !arel || !reflection else arel = virtual_aggregate_arel(reflection, method_name, column) end if arel virtual_attribute name, :integer, :uses => options[:uses] || relation, :arel => arel else virtual_attribute name, :integer, **options end end
define_virtual_aggregate_method(name, relation, column, ruby_method_name, arel_method_name = ruby_method_name) { |values| ... }
click to toggle source
# File lib/active_record/virtual_attributes/virtual_total.rb, line 99 def define_virtual_aggregate_method(name, relation, column, ruby_method_name, arel_method_name = ruby_method_name) define_method(name) do if has_attribute?(name) self[name] || 0 elsif (rel = send(relation)).loaded? values = rel.map { |t| t.send(column) }.compact if block_given? yield values else values.blank? ? nil : values.send(ruby_method_name) end else rel.try(arel_method_name, column) || 0 end end end
virtual_aggregate(name, relation, method_name = :sum, column = nil, options = {})
click to toggle source
@param method_name
:count :average :minimum :maximum :sum
example:
class Hardware has_many :disks virtual_sum :allocated_disk_storage, :disks, :size end generates: def allocated_disk_storage if disks.loaded? disks.map(&:size).compact.sum else disks.sum(:size) || 0 end end virtual_attribute :allocated_disk_storage, :integer, :uses => :disks, :arel => ... # arel => (SELECT sum("disks"."size") where "hardware"."id" = "disks"."hardware_id")
# File lib/active_record/virtual_attributes/virtual_total.rb, line 76 def virtual_aggregate(name, relation, method_name = :sum, column = nil, options = {}) return virtual_total(name, relation, options) if method_name == :size return virtual_sum(name, relation, column, options) if method_name == :sum end
virtual_aggregate_arel(reflection, method_name, column)
click to toggle source
# File lib/active_record/virtual_attributes/virtual_total.rb, line 116 def virtual_aggregate_arel(reflection, method_name, column) return unless reflection && reflection.macro == :has_many # need db access for the reflection join_keys, so delaying all this key lookup until call time lambda do |t| # strings and symbols are converted across, arel objects are not column = reflection.klass.arel_attribute(column) unless column.respond_to?(:count) # query: SELECT COUNT(*) FROM main_table JOIN foreign_table ON main_table.id = foreign_table.id JOIN ... relation_query = joins(reflection.name).select(column.send(method_name)) query = relation_query.arel # algorithm: # - remove main_table from this sub query. (it is already in the primary query) # - move the foreign_table from the JOIN to the FROM clause # - move the main_table.id = foreign_table.id from the ON clause to the WHERE clause # query: SELECT COUNT(*) FROM main_table [ ] JOIN ... join = query.source.right.shift # query: SELECT COUNT(*) FROM [foreign_table] JOIN ... query.source.left = join.left # query: SELECT COUNT(*) FROM foreign_table JOIN ... [WHERE main_table.id = foreign_table.id] query.where(join.right.expr) # convert bind variables from ? to actual values. otherwise, sql is incomplete conn = connection sql = if ActiveRecord.version.to_s >= "5.2" conn.unprepared_statement { conn.to_sql(query) } else conn.unprepared_statement { conn.to_sql(query, relation_query.bound_attributes) } end # add () around query query = t.grouping(Arel::Nodes::SqlLiteral.new(sql)) # add coalesce to ensure correct value comes out t.grouping(Arel::Nodes::NamedFunction.new('COALESCE', [query, Arel::Nodes::SqlLiteral.new("0")])) end end
virtual_average(name, relation, column, options = {})
click to toggle source
# File lib/active_record/virtual_attributes/virtual_total.rb, line 47 def virtual_average(name, relation, column, options = {}) define_virtual_aggregate_attribute(name, relation, :average, column, options) define_virtual_aggregate_method(name, relation, column, :average) { |values| values.count == 0 ? 0 : values.sum / values.count } end
virtual_maximum(name, relation, column, options = {})
click to toggle source
# File lib/active_record/virtual_attributes/virtual_total.rb, line 42 def virtual_maximum(name, relation, column, options = {}) define_virtual_aggregate_attribute(name, relation, :maximum, column, options) define_virtual_aggregate_method(name, relation, column, :max, :maximum) end
virtual_minimum(name, relation, column, options = {})
click to toggle source
# File lib/active_record/virtual_attributes/virtual_total.rb, line 37 def virtual_minimum(name, relation, column, options = {}) define_virtual_aggregate_attribute(name, relation, :minimum, column, options) define_virtual_aggregate_method(name, relation, column, :min, :minimum) end
virtual_sum(name, relation, column, options = {})
click to toggle source
# File lib/active_record/virtual_attributes/virtual_total.rb, line 32 def virtual_sum(name, relation, column, options = {}) define_virtual_aggregate_attribute(name, relation, :sum, column, options) define_virtual_aggregate_method(name, relation, column, :sum) end
virtual_total(name, relation, options = {})
click to toggle source
define an attribute to calculate the total of a has many relationship
example: class ExtManagementSystem has_many :vms virtual_total :total_vms, :vms end generates: def total_vms vms.count end virtual_attribute :total_vms, :integer, :uses => :vms, :arel => ... # arel == (SELECT COUNT(*) FROM vms where ems.id = vms.ems_id)
# File lib/active_record/virtual_attributes/virtual_total.rb, line 27 def virtual_total(name, relation, options = {}) define_virtual_aggregate_attribute(name, relation, :count, Arel.star, options) define_method(name) { (has_attribute?(name) ? self[name] : send(relation).try(:size)) || 0 } end