update之类的方法,分布如下

irb(main):014:0> Product.methods.grep(/^update/)
=> [:update_counters, :update_all, :update]
irb(main):015:0> Product.all.methods.grep /^update/
=> [:update, :update_all]
irb(main):033:0> Product.first.methods.grep(/^update/).reject{|m| m =~ /updated/ }
=> [:update, :update!, :update_attribute, :update_attributes, :update_attributes!, :update_column, :update_columns]
irb(main):050:0> Product.first.line_items.methods.grep /^update/
=> [:update_all, :update]


那些类方法,是这样定义的

module ActiveRecord
  module Querying
    delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all

module ActiveRecord
  class Base
    extend Querying


可以查得

irb(main):011:0> puts Product.singleton_class.ancestors.select{|a| a.to_s =~ /Q/}
ActiveRecord::Querying
ActiveRecord::QueryCache::ClassMethods


update_all

关于update_all,API的描述如下

Updates all records in the current relation with details given. This method constructs a single SQL UPDATE statement and sends it straight to the database. It does not instantiate the involved models and it does not trigger Active Record callbacks or validations. However, values passed to update_all will still go through Active Record's normal type casting and serialization

试验一下

irb(main):012:0> ps = Product.where(id: [1, 2])
  Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE "products"."id" IN (1, 2)
=> #<activerecord::relation [#<product="" id:="" 1,="" title:="" "seven="" mobile="" apps="" in="" seven="" weeks",="" description:="" "wtf",="" image_url:="" "7apps.jpg",="" price:="" 0.2e3,="" created_at:="" "2017-04-08="" 14:40:32",="" updated_at:="" "2017-06-11="" 03:21:59"="">, #<product id:="" 2,="" title:="" "ffdgfnhnghjjmjh",="" description:="" "hyjyju",="" image_url:="" "7apps.jpg",="" price:="" 0.6e1,="" created_at:="" "2017-04-08="" 14:40:32",="" updated_at:="" "2017-04-08="" 14:40:32"="">]>
irb(main):014:0> ps.map{|p| p.price.to_f}
=> [200.0, 6.0]
irb(main):015:0> binding.trace_tree(html: true, tmp: 'rails/update.html'){ ps.update price: 100 }
   (19.8ms)  begin transaction
  Product Exists (24.9ms)  SELECT  1 AS one FROM "products" WHERE "products"."title" = ? AND ("products"."id" != ?) LIMIT ?  [["title", "Seven Mobile Apps in Seven Weeks"], ["id", 1], ["LIMIT", 1]]
  SQL (26.1ms)  UPDATE "products" SET "price" = ?, "updated_at" = ? WHERE "products"."id" = ?  [["price", 0.1e3], ["updated_at", 2017-06-11 03:26:36 UTC], ["id", 1]]
   (15.8ms)  commit transaction
   (14.3ms)  begin transaction
  Product Exists (30.4ms)  SELECT  1 AS one FROM "products" WHERE "products"."title" = ? AND ("products"."id" != ?) LIMIT ?  [["title", "ffdgfnhnghjjmjh"], ["id", 2], ["LIMIT", 1]]
  SQL (30.8ms)  UPDATE "products" SET "price" = ?, "updated_at" = ? WHERE "products"."id" = ?  [["price", 0.1e3], ["updated_at", 2017-06-11 03:26:38 UTC], ["id", 2]]
   (14.7ms)  commit transaction
=> [#<product id:="" 1,="" title:="" "seven="" mobile="" apps="" in="" seven="" weeks",="" description:="" "wtf",="" image_url:="" "7apps.jpg",="" price:="" 0.1e3,="" created_at:="" "2017-04-08="" 14:40:32",="" updated_at:="" "2017-06-11="" 03:26:36"="">, #<product id:="" 2,="" title:="" "ffdgfnhnghjjmjh",="" description:="" "hyjyju",="" image_url:="" "7apps.jpg",="" price:="" 0.1e3,="" created_at:="" "2017-04-08="" 14:40:32",="" updated_at:="" "2017-06-11="" 03:26:38"="">]
irb(main):016:0> ps.map{|p| p.price.to_f}
=> [100.0, 100.0]
irb(main):017:0> binding.trace_tree(html: true, tmp: 'rails/update_all.html'){ ps.update_all price: 200 }
  SQL (63.5ms)  UPDATE "products" SET "price" = 200 WHERE "products"."id" IN (1, 2)
=> 2
irb(main):018:0> ps.map{|p| p.price.to_f}
=> [100.0, 100.0]
irb(main):021:0> ps.reload.map{|p| p.price.to_f}
  Product Load (0.3ms)  SELECT "products".* FROM "products" WHERE "products"."id" IN (1, 2)
=> [200.0, 200.0]</product></product></product></activerecord::relation>


各自完整调用栈如下

20170611_112635_619_update.html

20170611_112712_195_update_all.html

直观来看,update_all首先符合了does not instantiate the involved models的说法,有执行sql,但ActiveRecord::Relation中的对象的属性没有改变

其源码如下,一上来就是拼接sql,然后直接执行,因此,对已加载的model对象的属性没有任何影响,也没触发model的callbacks和validations

# activerecord-5.0.2/lib/active_record/relation.rb
def update_all(updates)
  raise ArgumentError, "Empty list of attributes to change" if updates.blank?

  stmt = Arel::UpdateManager.new

  stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
  stmt.table(table)

  if joins_values.any?
    @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
  else
    stmt.key = arel_attribute(primary_key)
    stmt.take(arel.limit)
    stmt.order(*arel.orders)
    stmt.wheres = arel.constraints
  end

  @klass.connection.update stmt, 'SQL', bound_attributes
end


update

relation的update是逐个取出relation中的对象,然后在每个对象上调update,它会assign_attributes并save,所以,model的属性是更新了,而且save中也会跑callbacks和validations


关键代码如下

activerecord-5.0.2/lib/active_record/relation.rb

def update(id = :all, attributes)
  if id.is_a?(Array)
    id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
  elsif id == :all
    records.each { |record| record.update(attributes) }
  else
    if ActiveRecord::Base === id
      id = id.id
      ActiveSupport::Deprecation.warn(<<-MSG.squish)
        You are passing an instance of ActiveRecord::Base to `update`.
        Please pass the id of the object by calling `.id`.
      MSG
    end
    object = find(id)
    object.update(attributes)
    object
  end
end


activerecord-5.0.2/lib/active_record/persistence.rb

def update(attributes)
  # The following transaction covers any possible database side-effects of the
  # attributes assignment. For example, setting the IDs of a child collection.
  with_transaction_returning_status do
    assign_attributes(attributes)
    save
  end
end


update_attribute

如下,注释很明确,虽然走的是save,但validate: false

# activerecord-5.0.2/lib/active_record/persistence.rb
#
# Updates a single attribute and saves the record.
# This is especially useful for boolean flags on existing records. Also note that
#
# * Validation is skipped.
# * \Callbacks are invoked.
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
# This method raises an ActiveRecord::ActiveRecordError  if the
# attribute is marked as readonly.
#
# Also see #update_column.
def update_attribute(name, value)
  name = name.to_s
  verify_readonly_attribute(name)
  public_send("#{name}=", value)

  changed? ? save(validate: false) : true
end


update_attributes

也就是update

alias update_attributes update


update_column与update_columns

两者其实干的是一回事:调用relation的update_all(无callbacks和validations),但会更新model的属性

# activerecord-5.0.2/lib/active_record/persistence.rb
def update_column(name, value)
  update_columns(name => value)
end

# Updates the attributes directly in the database issuing an UPDATE SQL
# statement and sets them in the receiver:
#
#   user.update_columns(last_request_at: Time.current)
#
# This is the fastest way to update attributes because it goes straight to
# the database, but take into account that in consequence the regular update
# procedures are totally bypassed. In particular:
#
# * \Validations are skipped.
# * \Callbacks are skipped.
# * +updated_at+/+updated_on+ are not updated.
# * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all
#
# This method raises an ActiveRecord::ActiveRecordError when called on new
# objects, or when at least one of the attributes is marked as readonly.
def update_columns(attributes)
  raise ActiveRecordError, "cannot update a new record" if new_record?
  raise ActiveRecordError, "cannot update a destroyed record" if destroyed?

  attributes.each_key do |key|
    verify_readonly_attribute(key.to_s)
  end

  updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes)

  attributes.each do |k, v|
    raw_write_attribute(k, v)
  end

  updated_count == 1
end


稍微试验一下

irb(main):042:0> p1 = Product.first
  Product Load (0.1ms)  SELECT  "products".* FROM "products" ORDER BY "products"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<product id:="" 1,="" title:="" "seven="" mobile="" apps="" in="" seven="" weeks",="" description:="" "wtf",="" image_url:="" "7apps.jpg",="" price:="" 0.2e3,="" created_at:="" "2017-04-08="" 14:40:32",="" updated_at:="" "2017-06-11="" 03:26:36"="">
irb(main):044:0> p1.price.to_f
=> 200.0
irb(main):045:0> p1.update_columns price: 300
  SQL (2.3ms)  UPDATE "products" SET "price" = 300 WHERE "products"."id" = ?  [["id", 1]]
=> true
irb(main):046:0> p1.price.to_f
=> 300.0</product>


一个简单总结如下