Upgrade to Pro — share decks privately, control downloads, hide ads and more …

An Intervention for ActiveRecord

An Intervention for ActiveRecord

Given at RailsConf 2013.

Let's be honest: ActiveRecord's got issues, and it's not going to deal with them on its own. It needs our help.

Don't think so? Let's take a closer look together. We'll examine the myriad of perils and pitfalls that await newbie and veteran alike, ranging from intentionally inconsistent behavior to subtle oddities arising from implementation details.

Of course, as with any intervention, we're only doing this because we care. At the very least, you'll learn something you didn't know about ActiveRecord, that helps you avoid these gotchas in your applications. But I hope that you'll leave inspired to contribute to ActiveRecord, engage in discussion about its direction with the core team, and therefore improve the lives of your fellow Rails developers.

WARNING: We will be reading the ActiveRecord code in this talk. Not for the faint of heart.

Ernie Miller

April 30, 2013
Tweet

More Decks by Ernie Miller

Other Decks in Technology

Transcript

  1. we’re hiring.* * “we’re hiring” is not part of the

    LivingSocial brand, but totally should be
  2. WARNING THE WARNING IN THE TALK DESCRIPTION, WHICH STATED THAT

    THERE WOULD BE ACTIVERECORD SOURCE CODE IN THIS TALK, WAS NOT A SCARE TACTIC.
  3. WARNING THE WARNING IN THE TALK DESCRIPTION, WHICH STATED THAT

    THERE WOULD BE ACTIVERECORD SOURCE CODE IN THIS TALK, WAS NOT A SCARE TACTIC. WE WILL BE READING ACTIVERECORD SOURCE CODE IN THIS TALK.
  4. WARNING THE WARNING IN THE TALK DESCRIPTION, WHICH STATED THAT

    THERE WOULD BE ACTIVERECORD SOURCE CODE IN THIS TALK, WAS NOT A SCARE TACTIC. WE WILL BE READING ACTIVERECORD SOURCE CODE IN THIS TALK. A LOT OF IT.
  5. WARNING THE WARNING IN THE TALK DESCRIPTION, WHICH STATED THAT

    THERE WOULD BE ACTIVERECORD SOURCE CODE IN THIS TALK, WAS NOT A SCARE TACTIC. WE WILL BE READING ACTIVERECORD SOURCE CODE IN THIS TALK. A LOT OF IT. THINGS MIGHT GET CRAZY.
  6. WARNING THE WARNING IN THE TALK DESCRIPTION, WHICH STATED THAT

    THERE WOULD BE ACTIVERECORD SOURCE CODE IN THIS TALK, WAS NOT A SCARE TACTIC. WE WILL BE READING ACTIVERECORD SOURCE CODE IN THIS TALK. A LOT OF IT. THINGS MIGHT GET CRAZY. WILL
  7. WARNING THE WARNING IN THE TALK DESCRIPTION, WHICH STATED THAT

    THERE WOULD BE ACTIVERECORD SOURCE CODE IN THIS TALK, WAS NOT A SCARE TACTIC. WE WILL BE READING ACTIVERECORD SOURCE CODE IN THIS TALK. A LOT OF IT. THERE IS STILL TIME TO ESCAPE. THINGS MIGHT GET CRAZY. WILL
  8. “An object that wraps a row in a database table

    or view, encapsulates the database access, and adds domain logic on that data.” A PATTERN
  9. “Active Record has the primary advantage of simplicity. It’s easy

    to build Active Records, and they are easy to understand.” A PATTERN
  10. A LIBRARY class User < ActiveRecord::Base # name, email, and

    timestamps end User.ancestors.size # => 57 User.methods.size # => 559 User.instance_methods.size # => 352
  11. A LIBRARY $ cloc lib/active_record ------------------------------ Language files code ------------------------------

    Ruby 147 15836 YAML 1 11 ------------------------------ SUM: 148 15847 ------------------------------
  12. “Active Record has the primary advantage of simplicity. It’s easy

    to build Active Records, and they are easy to understand.”
  13. $ cloc lib/active_record ------------------------------ Language files code ------------------------------ Ruby 147

    15836 YAML 1 11 ------------------------------ SUM: 148 15847 ------------------------------ User.ancestors.size # => 57 User.methods.size # => 559 User.instance_methods.size # => 352
  14. BUT

  15. Library LOC ActionMailer 527 ActiveModel 1,601 ActionController 2,557 ActionView 6,317

    ActionDispatch 7,313 ActiveSupport 9,150 ActiveRecord 15,847
  16. HOW TO CREATE A LEARNING CURVE 1.Find things your users

    understand. 2.Ignore those things.
  17. class User < ActiveRecord::Base def initialize(*) super self.name ||= '<name

    not provided>' end end user = User.new user.name # => "<name not provided>"
  18. class User < ActiveRecord::Base def initialize(*) super self.name ||= '<name

    not provided>' end end user = User.new user.name # => "<name not provided>" user = User.where(name: nil).first user.name # => nil
  19. class User < ActiveRecord::Base after_initialize :set_defaults def set_defaults self.name ||=

    '<name not provided>' end end user = User.new user.name # => "<name not provided>" user = User.where(name: nil).first user.name # => "<name not provided>"
  20. core.rb: def initialize(attributes = nil) defaults = self.class.column_defaults.dup defaults.each {

    |k, v| defaults[k] = v.dup if v.duplicable? } @attributes = self.class.initialize_attributes(defaults) @columns_hash = self.class.column_types.dup init_internals init_changed_attributes ensure_proper_type populate_with_current_scope_attributes assign_attributes(attributes) if attributes yield self if block_given? run_callbacks :initialize unless _initialize_callbacks.empty? end
  21. core.rb: def init_with(coder) @attributes = self.class.initialize_attributes( coder['attributes'] ) @columns_hash =

    self.class.column_types.merge( coder['column_types'] || {} ) init_internals @new_record = false run_callbacks :find run_callbacks :initialize self end
  22. core.rb: def init_with(coder) @attributes = self.class.initialize_attributes( coder['attributes'] ) @columns_hash =

    self.class.column_types.merge( coder['column_types'] || {} ) init_internals @new_record = false run_callbacks :find run_callbacks :initialize self end
  23. core.rb: def init_with(coder) @attributes = self.class.initialize_attributes( coder['attributes'] ) @columns_hash =

    self.class.column_types.merge( coder['column_types'] || {} ) init_internals @new_record = false run_callbacks :find run_callbacks :initialize self end
  24. persistence.rb: def instantiate(record, column_types = {}) klass = discriminate_class_for_record(record) column_types

    = klass.decorate_columns(column_types) klass.allocate.init_with( 'attributes' => record, 'column_types' => column_types ) end def find_by_sql(sql, binds = []) result_set = connection.select_all( sanitize_sql(sql), "#{name} Load", binds ) column_types = result_set.column_types result_set.map { |record| instantiate(record, column_types) } end querying.rb: * * Deprecation warning omitted
  25. persistence.rb: def instantiate(record, column_types = {}) klass = discriminate_class_for_record(record) column_types

    = klass.decorate_columns(column_types) klass.allocate.init_with( 'attributes' => record, 'column_types' => column_types ) end def find_by_sql(sql, binds = []) result_set = connection.select_all( sanitize_sql(sql), "#{name} Load", binds ) column_types = result_set.column_types result_set.map { |record| instantiate(record, column_types) } end querying.rb: * * Deprecation warning omitted
  26. persistence.rb: def instantiate(record, column_types = {}) klass = discriminate_class_for_record(record) column_types

    = klass.decorate_columns(column_types) klass.allocate.init_with( 'attributes' => record, 'column_types' => column_types ) end def find_by_sql(sql, binds = []) result_set = connection.select_all( sanitize_sql(sql), "#{name} Load", binds ) column_types = result_set.column_types result_set.map { |record| instantiate(record, column_types) } end querying.rb: * * Deprecation warning omitted
  27. class User < ActiveRecord::Base has_many :posts def assign_posts(post_or_posts) posts =

    Array(post_or_posts) posts.each { |post| post.published = false } self.posts += posts end end user.assign_posts posts user.posts.all? { |post| post.user_id == user.id } Q:
  28. A:

  29. I HAVE ABSOLUTELY NO IDEA. A: Extra credit: “You’re not

    even testing the right attribute, genius.”
  30. class User < ActiveRecord::Base has_many :posts has_many :published_posts, -> {

    where published: true }, class_name: 'Post' end User.joins(:posts).where(posts: {title: 'zomg first post!'}) SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "posts"."title" = 'zomg first post!'
  31. class User < ActiveRecord::Base has_many :posts has_many :published_posts, -> {

    where published: true }, class_name: 'Post' end User.joins(:posts).where(posts: {title: 'zomg first post!'}) User.joins(:published_posts). where(published_posts: {title: 'zomg first post!'}) SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "posts"."title" = 'zomg first post!'
  32. class User < ActiveRecord::Base has_many :posts has_many :published_posts, -> {

    where published: true }, class_name: 'Post' end User.joins(:posts).where(posts: {title: 'zomg first post!'}) User.joins(:published_posts). where(published_posts: {title: 'zomg first post!'}) SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "posts"."title" = 'zomg first post!' ActiveRecord::StatementInvalid
  33. relation/query_methods.rb: def build_where(opts, other = []) case opts when String,

    Array [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) attributes.values.grep(ActiveRecord::Relation) do |rel| self.bind_values += rel.bind_values end PredicateBuilder.build_from_hash(klass, attributes, table) else [opts] end end
  34. relation/query_methods.rb: def build_where(opts, other = []) case opts when String,

    Array [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) attributes.values.grep(ActiveRecord::Relation) do |rel| self.bind_values += rel.bind_values end PredicateBuilder.build_from_hash(klass, attributes, table) else [opts] end end
  35. relation/predicate_builder.rb: def self.build_from_hash(klass, attributes, default_table) queries = [] attributes.each do

    |column, value| table = default_table if value.is_a?(Hash) if value.empty? queries << '1=0' else table = Arel::Table.new(column, default_table.engine) association = klass.reflect_on_association(column.to_sym) value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) end end else # ... end end queries end
  36. relation/predicate_builder.rb: def self.build_from_hash(klass, attributes, default_table) queries = [] attributes.each do

    |column, value| table = default_table if value.is_a?(Hash) if value.empty? queries << '1=0' else table = Arel::Table.new(column, default_table.engine) association = klass.reflect_on_association(column.to_sym) value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) end end else # ... end end queries end
  37. class User < ActiveRecord::Base has_many :contributions has_many :posts, through: :contributions

    end class Post < ActiveRecord::Base has_many :contributions has_many :contributors, through: :contributions, source: :user end class Contribution < ActiveRecord::Base belongs_to :user belongs_to :post validates :role, presence: true end
  38. class User < ActiveRecord::Base has_many :contributions has_many :posts, through: :contributions

    end class Post < ActiveRecord::Base has_many :contributions has_many :contributors, through: :contributions, source: :user end class Contribution < ActiveRecord::Base belongs_to :user belongs_to :post validates :role, presence: true end
  39. post = Post.new(params[:post]) post.contributors = [current_user] post.contributions.first.role = 'author' #

    => NoMethodError: undefined method `role=' for nil:NilClass post = Post.new(params[:post]) post.contributors = [current_user] post.save # => false
  40. post = Post.new(params[:post]) post.contributors = [current_user] post.contributions.first.role = 'author' #

    => NoMethodError: undefined method `role=' for nil:NilClass contribution = current_user.contributions.build(role: 'author') contribution.build_post(params[:post]) contribution.save # => true post = Post.new(params[:post]) post.contributors = [current_user] post.save # => false
  41. class Post < ActiveRecord::Base has_many :contributions has_many :contributors, through: :contributions,

    source: :user has_many :authorships, -> { where role: 'author' }, class_name: 'Contribution' has_many :authors, through: :authorships, source: :user end post = Post.new(params[:post]) post.authors = [current_user] post.save # => true
  42. module ActiveRecord module Associations class HasManyThroughAssociation < HasManyAssociation # ...

    def concat_records(records) ensure_not_nested records = super if owner.new_record? && records records.flatten.each do |record| build_through_record(record) end end records end # ... end end end
  43. module ActiveRecord module Associations class HasManyThroughAssociation < HasManyAssociation # ...

    def concat_records(records) ensure_not_nested records = super if owner.new_record? && records records.flatten.each do |record| build_through_record(record) end end records end # ... end end end
  44. class Contribution < ActiveRecord::Base belongs_to :user belongs_to :post validates :user_id,

    presence: true validates :post_id, presence: true end post = Post.new(params[:post]) post.contributors = [current_user] post.save # => false
  45. class Contribution < ActiveRecord::Base belongs_to :user belongs_to :post validates :user_id,

    presence: true validates :post_id, presence: true end post = Post.new(params[:post]) post.contributors = [current_user] post.save # => false
  46. class Contribution < ActiveRecord::Base belongs_to :user belongs_to :post validates :user_id,

    presence: true validates :post_id, presence: true end post = Post.new(params[:post]) post.contributors = [current_user] post.save # => false post.errors.messages # => {:contributions => ["is invalid"]}
  47. class Post < ActiveRecord::Base has_many :contributions, inverse_of: :post has_many :contributors,

    through: :contributions, source: :user end class User < ActiveRecord::Base has_many :contributions, inverse_of: :user has_many :posts, through: :contributions end class Contribution < ActiveRecord::Base belongs_to :user belongs_to :post validates :user, presence: true validates :post, presence: true end
  48. class Post < ActiveRecord::Base has_many :contributions, inverse_of: :post has_many :contributors,

    through: :contributions, source: :user end class User < ActiveRecord::Base has_many :contributions, inverse_of: :user has_many :posts, through: :contributions end class Contribution < ActiveRecord::Base belongs_to :user belongs_to :post validates :user, presence: true validates :post, presence: true end
  49. class Post < ActiveRecord::Base has_many :contributions, inverse_of: :post has_many :contributors,

    through: :contributions, source: :user end class User < ActiveRecord::Base has_many :contributions, inverse_of: :user has_many :posts, through: :contributions end class Contribution < ActiveRecord::Base belongs_to :user belongs_to :post validates :user, presence: true validates :post, presence: true end
  50. “Using this validation method [...] does not guarantee the absence

    of duplicate record insertions.” ― validations/uniqueness.rb
  51. “The best way to work around this problem is to

    add a unique index to the database table.” ― validations/uniqueness.rb
  52. class User < ActiveRecord::Base validates :email, uniqueness: true end class

    CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :name t.string :email end add_index :users, :email, unique: true end end
  53. class User < ActiveRecord::Base def save(*) super rescue ActiveRecord::RecordNotUnique =>

    e errors.add(:email, :taken) false end end User.create name: 'Ernie Miller', email: '[email protected]' # => true user = User.create name: 'Ernie Miller', email: '[email protected]' # => false user.errors.messages # => {:email => ["has already been taken"]}
  54. class Post < ActiveRecord::Base def save(*) super rescue ActiveRecord::RecordNotUnique =>

    e attr_name, *scope = e.message. match(/Key \(([^\)]+)\)/)[1].split(/,\s*/) message = errors.generate_message(attr_name, :taken) if scope.any? message << " (scope: #{scope.to_sentence})" end errors.add attr_name, message false end end post = Post.create(title: 'Hello', subtitle: 'World!') # => false post.errors.messages # => {:title => ["has already been taken (scope: subtitle)"]}
  55. class Post < ActiveRecord::Base def save(*) super rescue ActiveRecord::RecordNotUnique =>

    e attr_name, *scope = e.message. match(/Key \(([^\)]+)\)/)[1].split(/,\s*/) message = errors.generate_message(attr_name, :taken) if scope.any? message << " (scope: #{scope.to_sentence})" end errors.add attr_name, message false end end post = Post.create(title: 'Hello', subtitle: 'World!') # => false post.errors.messages # => {:title => ["has already been taken (scope: subtitle)"]}
  56. def merged_wheres values[:where] ||= [] if values[:where].empty? || relation.where_values.empty? relation.where_values

    + values[:where] else # Remove equalities from the existing relation with a LHS which is # present in the relation being merged in. seen = Set.new values[:where].each { |w| if w.respond_to?(:operator) && w.operator == :== seen << w.left end } relation.where_values.reject { |w| w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left) } + values[:where] end end relation/merger.rb:
  57. # We need to be able to support merging two

    relations without having # to get our hooks too deeply into Active Record. That proves to be # easier said than done. I hate Relation#merge. If Squeel has a # nemesis, Relation#merge would be it. # # Whatever code you see here currently is my current best attempt at # coexisting peacefully with said nemesis. def merge(r, equalities_resolved = false) if ::ActiveRecord::Relation === r && !equalities_resolved if self.table_name != r.table_name super(r.visited) else merge_resolving_duplicate_squeel_equalities(r) end else super(r) end end squeel/adapters/active_record/relation_extensions.rb:
  58. SO

  59. Relation#exec_queries def exec_queries default_scoped = with_default_scope if default_scoped.equal?(self) @records =

    eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) preload = preload_values preload += includes_values unless eager_loading? preload.each do |associations| ActiveRecord::Associations::Preloader.new(@records, associations).run end # @readonly_value is true only if set explicitly. @implicit_readonly is true if there # are JOINS and no explicit SELECT. readonly = readonly_value.nil? ? @implicit_readonly : readonly_value @records.each { |record| record.readonly! } if readonly else @records = default_scoped.to_a end @loaded = true @records end relation.rb:
  60. Relation#exec_queries def exec_queries default_scoped = with_default_scope if default_scoped.equal?(self) @records =

    eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) preload = preload_values preload += includes_values unless eager_loading? preload.each do |associations| ActiveRecord::Associations::Preloader.new(@records, associations).run end # @readonly_value is true only if set explicitly. @implicit_readonly is true if there # are JOINS and no explicit SELECT. readonly = readonly_value.nil? ? @implicit_readonly : readonly_value @records.each { |record| record.readonly! } if readonly else @records = default_scoped.to_a end @loaded = true @records end relation.rb:
  61. Relation#with_default_scope def with_default_scope #:nodoc: if default_scoped? && default_scope = klass.send(:build_default_scope)

    default_scope = default_scope.merge(self) default_scope.default_scoped = false default_scope else self end end relation.rb:
  62. Relation#with_default_scope def with_default_scope #:nodoc: if default_scoped? && default_scope = klass.send(:build_default_scope)

    default_scope = default_scope.merge(self) default_scope.default_scoped = false default_scope else self end end relation.rb:
  63. Relation#build_default_scope def build_default_scope # :nodoc: if !Base.is_a?(method(:default_scope).owner) # The user

    has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| if !scope.is_a?(Relation) && scope.respond_to?(:call) default_scope.merge(unscoped { scope.call }) else default_scope.merge(scope) end end end end end scoping/default.rb:
  64. Relation#build_default_scope def build_default_scope # :nodoc: if !Base.is_a?(method(:default_scope).owner) # The user

    has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| if !scope.is_a?(Relation) && scope.respond_to?(:call) default_scope.merge(unscoped { scope.call }) else default_scope.merge(scope) end end end end end scoping/default.rb:
  65. Relation#build_default_scope def build_default_scope # :nodoc: if !Base.is_a?(method(:default_scope).owner) # The user

    has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| if !scope.is_a?(Relation) && scope.respond_to?(:call) default_scope.merge(unscoped { scope.call }) else default_scope.merge(scope) end end end end end scoping/default.rb:
  66. scope(name, scope_options = {}) “Adds a class method for retrieving

    and querying objects. A scope represents a narrowing of a database query.” ― scoping/named.rb
  67. class User < ActiveRecord::Base default_scope -> { where(name: 'Jim') }

    scope :bobs, -> { where(name: 'Bob') } scope :toms, -> { where(name: 'Tom') } end User.where(name: 'Joe') User.bobs User.bobs.toms User.bobs.toms.where(name: 'Joe') User.where(name: 'Joe').bobs => Joe => Bob => Tom => Tom, Joe => Bob 4.0.0.beta1
  68. class User < ActiveRecord::Base default_scope -> { where(name: 'Jim') }

    scope :bobs, -> { where(name: 'Bob') } scope :toms, -> { where(name: 'Tom') } end User.where(name: 'Joe') User.bobs User.bobs.toms User.bobs.toms.where(name: 'Joe') User.where(name: 'Joe').bobs => Joe => Bob => Bob, Tom => Bob, Tom, Joe => Joe, Bob 4.0.0.rc1
  69. class Post < ActiveRecord::Base belongs_to :user def self.number_of_pages(page_size = 20)

    number_of_posts = self.count full_pages = number_of_posts / page_size if (number_of_posts % page_size).zero? full_pages else # We will have a page with remainders full_pages + 1 end end end
  70. class Post < ActiveRecord::Base belongs_to :user def self.number_of_pages(page_size = 20)

    number_of_posts = self.count full_pages = number_of_posts / page_size if (number_of_posts % page_size).zero? full_pages else # We will have a page with remainders full_pages + 1 end end end NoMethodError: undefined method `/' for #<Hash:...>
  71. def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase distinct

    = self.distinct_value if operation == "count" column_name ||= (select_for_count || :all) unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true end column_name = primary_key if column_name == :all && distinct distinct = nil if column_name =~ /\s*DISTINCT\s+/i end if group_values.any? execute_grouped_calculation(operation, column_name, distinct) else execute_simple_calculation(operation, column_name, distinct) end end relation/calculations.rb: * * Deprecation warning omitted
  72. def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase distinct

    = self.distinct_value if operation == "count" column_name ||= (select_for_count || :all) unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true end column_name = primary_key if column_name == :all && distinct distinct = nil if column_name =~ /\s*DISTINCT\s+/i end if group_values.any? execute_grouped_calculation(operation, column_name, distinct) else execute_simple_calculation(operation, column_name, distinct) end end relation/calculations.rb: * * Deprecation warning omitted
  73. relation/calculations.rb: def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: group_attrs = group_values if

    group_attrs.first.respond_to?(:to_sym) association = @klass.reflect_on_association(group_attrs.first.to_sym) associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else group_fields = group_attrs end group_aliases = group_fields.map { |field| column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, field] } group = group_fields if operation == 'count' && column_name == :all aggregate_alias = 'count_all' else aggregate_alias = column_alias_for([operation, column_name].join(' ')) end select_values = [
  74. relation/calculations.rb: def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: group_attrs = group_values if

    group_attrs.first.respond_to?(:to_sym) association = @klass.reflect_on_association(group_attrs.first.to_sym) associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else group_fields = group_attrs end group_aliases = group_fields.map { |field| column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, field] } group = group_fields if operation == 'count' && column_name == :all aggregate_alias = 'count_all' else aggregate_alias = column_alias_for([operation, column_name].join(' ')) end select_values = [
  75. if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } key_records

    = association.klass.base_class.find(key_ids) key_records = Hash[key_records.map { |r| [r.id, r] }] end Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| column = calculated_data.column_types.fetch(aliaz) do column_for(col_name) end type_cast_calculated_value(row[aliaz], column) } key = key.first if key.size == 1 key = key_records[key] if associated [ key, type_cast_calculated_value( row[aggregate_alias], column_for(column_name), operation ) ] end] end
  76. if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } key_records

    = association.klass.base_class.find(key_ids) key_records = Hash[key_records.map { |r| [r.id, r] }] end Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| column = calculated_data.column_types.fetch(aliaz) do column_for(col_name) end type_cast_calculated_value(row[aliaz], column) } key = key.first if key.size == 1 key = key_records[key] if associated [ key, type_cast_calculated_value( row[aggregate_alias], column_for(column_name), operation ) ] end] end
  77. “That's a total of twelve callbacks, which gives you immense

    power to react and prepare for each state in the Active Record life cycle.” ― active_record/callbacks.rb
  78. class User < ActiveRecord::Base before_save NameSayer.new before_save :say_my_name before_save {

    |record| puts "My name is #{record.name}" } before_save 'puts "My name is #{self.name}"' # YES, REALLY def say_my_name puts "My name is #{self.name}" end end class NameSayer def before_save(record) puts "My name is #{record.name}" end end
  79. class User < ActiveRecord::Base before_save NameSayer.new before_save :say_my_name before_save {

    |record| puts "My name is #{record.name}" } before_save 'puts "My name is #{self.name}"' # YES, REALLY def say_my_name puts "My name is #{self.name}" end end class NameSayer def before_save(record) puts "My name is #{record.name}" end end
  80. class User < ActiveRecord::Base before_save NameSayer.new before_save :say_my_name before_save {

    |record| puts "My name is #{record.name}" } before_save 'puts "My name is #{self.name}"' # YES, REALLY def say_my_name puts "My name is #{self.name}" end end class NameSayer def before_save(record) puts "My name is #{record.name}" end end User.new._save_callbacks.class # => ActiveSupport::Callbacks::CallbackChain
  81. active_support/callbacks.rb: class CallbackChain < Array #:nodoc:# # ... def compile

    method = ["value = nil", "halted = false"] callbacks = "value = !halted && (!block_given? || yield)" reverse_each do |callback| callbacks = callback.apply(callbacks) end method << callbacks method << "value" method.join("\n") end # ... end
  82. active_support/callbacks.rb: class CallbackChain < Array #:nodoc:# # ... def compile

    method = ["value = nil", "halted = false"] callbacks = "value = !halted && (!block_given? || yield)" reverse_each do |callback| callbacks = callback.apply(callbacks) end method << callbacks method << "value" method.join("\n") end # ... end
  83. def apply(code) case @kind when :before <<-RUBY_EVAL if !halted &&

    #{@compiled_options} # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = #{@filter} halted = (#{chain.config[:terminator]}) if halted halted_callback_hook(#{@raw_filter.inspect.inspect}) end end #{code} RUBY_EVAL when :after <<-RUBY_EVAL #{code} if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} #{@filter} end RUBY_EVAL when :around name = define_conditional_callback <<-RUBY_EVAL active_support/callbacks.rb:
  84. def apply(code) case @kind when :before <<-RUBY_EVAL if !halted &&

    #{@compiled_options} # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = #{@filter} halted = (#{chain.config[:terminator]}) if halted halted_callback_hook(#{@raw_filter.inspect.inspect}) end end #{code} RUBY_EVAL when :after <<-RUBY_EVAL #{code} if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} #{@filter} end RUBY_EVAL when :around name = define_conditional_callback <<-RUBY_EVAL active_support/callbacks.rb:
  85. def apply(code) case @kind when :before <<-RUBY_EVAL if !halted &&

    #{@compiled_options} # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = #{@filter} halted = (#{chain.config[:terminator]}) if halted halted_callback_hook(#{@raw_filter.inspect.inspect}) end end #{code} RUBY_EVAL when :after <<-RUBY_EVAL #{code} if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} #{@filter} end RUBY_EVAL when :around name = define_conditional_callback <<-RUBY_EVAL active_support/callbacks.rb:
  86. def apply(code) case @kind when :before <<-RUBY_EVAL if !halted &&

    #{@compiled_options} # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = #{@filter} halted = (#{chain.config[:terminator]}) if halted halted_callback_hook(#{@raw_filter.inspect.inspect}) end end #{code} RUBY_EVAL when :after <<-RUBY_EVAL #{code} if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} #{@filter} end RUBY_EVAL when :around name = define_conditional_callback <<-RUBY_EVAL active_support/callbacks.rb:
  87. halted = (#{chain.config[:terminator]}) if halted halted_callback_hook(#{@raw_filter.inspect.inspect}) end end #{code} RUBY_EVAL

    when :after <<-RUBY_EVAL #{code} if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} #{@filter} end RUBY_EVAL when :around name = define_conditional_callback <<-RUBY_EVAL #{name}(halted) do #{code} value end RUBY_EVAL end end
  88. active_support/callbacks.rb: class Callback #:nodoc:# @@_callback_sequence = 0 attr_accessor :chain, :filter,

    :kind, :options, :klass, :raw_filter def initialize(chain, filter, kind, options, klass) @chain, @kind, @klass = chain, kind, klass deprecate_per_key_option(options) normalize_options!(options) @raw_filter, @options = filter, options @filter = _compile_filter(filter) recompile_options! end # ... end
  89. active_support/callbacks.rb: class Callback #:nodoc:# @@_callback_sequence = 0 attr_accessor :chain, :filter,

    :kind, :options, :klass, :raw_filter def initialize(chain, filter, kind, options, klass) @chain, @kind, @klass = chain, kind, klass deprecate_per_key_option(options) normalize_options!(options) @raw_filter, @options = filter, options @filter = _compile_filter(filter) recompile_options! end # ... end
  90. active_support/callbacks.rb: class Callback #:nodoc:# @@_callback_sequence = 0 attr_accessor :chain, :filter,

    :kind, :options, :klass, :raw_filter def initialize(chain, filter, kind, options, klass) @chain, @kind, @klass = chain, kind, klass deprecate_per_key_option(options) normalize_options!(options) @raw_filter, @options = filter, options @filter = _compile_filter(filter) recompile_options! end # ... end
  91. active_support/callbacks.rb: def recompile_options! conditions = ["true"] unless options[:if].empty? conditions <<

    Array(_compile_filter(options[:if])) end unless options[:unless].empty? conditions << Array(_compile_filter(options[:unless])). map {|f| "!#{f}"} end @compiled_options = conditions.flatten.join(" && ") end
  92. active_support/callbacks.rb: def recompile_options! conditions = ["true"] unless options[:if].empty? conditions <<

    Array(_compile_filter(options[:if])) end unless options[:unless].empty? conditions << Array(_compile_filter(options[:unless])). map {|f| "!#{f}"} end @compiled_options = conditions.flatten.join(" && ") end
  93. def _compile_filter(filter) @_is_object_filter = false case filter when Array filter.map

    {|f| _compile_filter(f)} when Symbol filter when String "(#{filter})" when Proc method_name = "_callback_#{@kind}_#{next_id}" @klass.send(:define_method, method_name, &filter) return method_name if filter.arity <= 0 method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ") else method_name = _method_name_for_object_filter(kind, filter) @_is_object_filter = true @klass.send(:define_method, "#{method_name}_object") { filter } _normalize_legacy_filter(kind, filter) scopes = Array(chain.config[:scope]) method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_") @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{method_name}(&blk) active_support/callbacks.rb:
  94. when String "(#{filter})" when Proc method_name = "_callback_#{@kind}_#{next_id}" @klass.send(:define_method, method_name,

    &filter) return method_name if filter.arity <= 0 method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ") else method_name = _method_name_for_object_filter(kind, filter) @_is_object_filter = true @klass.send(:define_method, "#{method_name}_object") { filter } _normalize_legacy_filter(kind, filter) scopes = Array(chain.config[:scope]) method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_") @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{method_name}(&blk) #{method_name}_object.send(:#{method_to_call}, self, &blk) end RUBY_EVAL method_name end end
  95. value = nil halted = false if !halted && true

    # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = _callback_before_15 halted = (result == false) if halted halted_callback_hook("#<NameSayer:0x007fa08a63c2e8>") end end if !halted && true # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = say_my_name halted = (result == false) if halted halted_callback_hook(":say_my_name") end end if !halted && true # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = _callback_before_17(self)
  96. if !halted && true # This double assignment is to

    prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = _callback_before_17(self) halted = (result == false) if halted halted_callback_hook("#<Proc:0x007fa08a642d78@...>") end end if !halted && true # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = (puts "My name is #{self.name}") halted = (result == false) if halted halted_callback_hook("\"puts \\\"My name is \\\#{self.name}\\\"\"") end end value = !halted && (!block_given? || yield) value
  97. if !halted && true # This double assignment is to

    prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = _callback_before_17(self) halted = (result == false) if halted halted_callback_hook("#<Proc:0x007fa08a642d78@...>") end end if !halted && true # This double assignment is to prevent warnings in 1.9.3 as # the `result` variable is not always used except if the # terminator code refers to it. result = result = (puts "My name is #{self.name}") halted = (result == false) if halted halted_callback_hook("\"puts \\\"My name is \\\#{self.name}\\\"\"") end end value = !halted && (!block_given? || yield) value
  98. active_support/callbacks.rb: module ClassMethods def __define_callbacks(kind, object) #:nodoc: name = __callback_runner_name(kind)

    unless object.respond_to?(name, true) str = object.send("_#{kind}_callbacks").compile class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{name}() #{str} end protected :#{name} RUBY_EVAL end name end end def run_callbacks(kind, &block) runner_name = self.class.__define_callbacks(kind, self) send(runner_name, &block) end
  99. active_record/callbacks.rb: module Callbacks def destroy #:nodoc: run_callbacks(:destroy) { super }

    end def touch(*) #:nodoc: run_callbacks(:touch) { super } end private def create_or_update #:nodoc: run_callbacks(:save) { super } end def create_record #:nodoc: run_callbacks(:create) { super } end def update_record(*) #:nodoc: run_callbacks(:update) { super } end end
  100. BEFORE FILTER!!! class User < ActiveRecord::Base def create_or_update say_my_name &&

    super end def say_my_name puts "My name is #{name}"; true end end
  101. BEFORE FILTER!!! class User < ActiveRecord::Base def create_or_update say_my_name &&

    super end def say_my_name puts "My name is #{name}"; true end end
  102. AFTER FILTER!!! class User < ActiveRecord::Base def create_or_update super &&

    final_stuff or raise ActiveRecord::Rollback end def final_stuff puts "IMPORTANT FINAL STUFF" end end
  103. AFTER FILTER!!! class User < ActiveRecord::Base def create_or_update super &&

    final_stuff or raise ActiveRecord::Rollback end def final_stuff puts "IMPORTANT FINAL STUFF" end end
  104. AROUND FILTER!!! class User < ActiveRecord::Base def create_or_update puts "GET

    READY" unless sneak_attack? super puts "WASN'T THAT EXCITING?" if awesome? end end
  105. AROUND FILTER!!! class User < ActiveRecord::Base def create_or_update puts "GET

    READY" unless sneak_attack? super puts "WASN'T THAT EXCITING?" if awesome? end end
  106. “That's a total of twelve callbacks, which gives you immense

    power to react and prepare for each state in the Active Record life cycle.” ― active_record/callbacks.rb
  107. “That’s Ruby, which gives you immense power to do, you

    know, pretty much anything, ever.” ― me
  108. SO?