Active Record 回调与数据查询接口


Active Record回调

介入Active Rccord 对象的生命周期

对象的生命周期:

在rails应用正常运作期间,对象可以被创建、更新或删除。Active Record为对象的生命周期提供了钩子,使我们可以控制应用及其数据。

回调:

我们可以在对象状态更改之前或之后触发逻辑

例:

class User < ApplicationRecord
before_validation :normalize_name, on: :create

# :on 选项的值也可以是数组
after_validation :set_location, on: [ :create, :update ]

private
  def normalize_name
    self.name = name.downcase.titleize
  end

  def set_location
    self.location = LocationService.query(self)
  end
end

通过:on :回调注册仅被某些生命周期时间触发。

通常应该把回调定义为私有方法。


可用的回调及执行顺序

创建对象:

  • before_validation

  • after_validation

  • before_save

  • around_save

  • before_create

  • around_create

  • after_create

  • after_save

  • after_commit/after_rollback

更新对象:

  • before_validation

  • after_validation

  • before_save

  • around_save

  • before_update

  • around_update

  • after_update

  • after_save

  • after_commit/after_rollback

删除对象:

  • before_destory

  • around_destory

  • after_destory

  • after_commit/after_rlooback

无论按什么顺序注册回调,在创建和更新对象时,after_save回调总是在更明确的after_create和after_update回调之后被调用。

after_initialize和after_find回调

对象呗实例化时,不管是直接new还是从数据库中加载,都会调用after_initialize回调。避免覆盖Active Record的initilalize方法。

如果同时定义了 after_initialize 和 after_find回调,会先调用 after_find 回调。

after_initialize 和 after_find 回调没有对应的 before_* 回调,这两个回调的注册方式和其他 Active Record 回调一样。

after_touch回调

当我们在 Active Record 对象上调用 touch 方法时,会调用 after_touch 回调。


调用回调

下面的方法会触发回调:

  • create

  • create!

  • decrement!

  • destory

  • destory!

  • destory_all

  • increment!

  • save

  • save!

  • save(validate:false)

  • toggle!

  • update_attribute

  • update

  • update!

  • valid?

下面这些会触发after_find回调:

  • all

  • first

  • find

  • find_by

  • find_by_*

  • find_by_*!

  • find_by_sql

  • last

每次初始化类的新对象时都会触发after_initilalize回调。


跳过回调

使用下面这些方法可以跳过回调:

  • decrement

  • decrement_count

  • delete

  • delete_all

  • increment

  • increment_count

  • toggle

  • touch

  • update_column

  • update_columns

  • update_all

  • update_counters

慎重使用,跳过回调,可能导致无效数据。


停止执行

回调在模型中注册后,将被加入队列等待执行。这个队列包含了所有模型的验证、已注册的回调和将要执行的数据库操作。

整个回调链包装在一个事物中。只要有回调抛出异常,回调链随即停止,并且发出ROLLBACK消息。

你也可以主动停止回调链:

throw:abord

关联回调

通过模型之间的关联,执行关联模型对象的相关回调操作。

假设有一个用户在博客中发表了多篇文章,现在我们要删除这个用户,那么这个用户的所有文章也应该删除,为此我们通过 Article模型和 User 模型的关联来给 User模型添加一个 after_destroy回调:

class User < ApplicationRecord
has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
after_destroy :log_destroy_action

def log_destroy_action
  puts 'Article destroyed'
end
end
?
>> user = User.first
=> #
>> user.articles.create!
=> #

>> user.destroy
Article destroyed
=> #

条件回调

和条件验证一样,可以在满足条件的情况下再调用回调方法。

class Comment < ApplicationRecord
after_create :send_email_to_author, if: :author_wants_emails?,
  unless: Proc.new { |comment| comment.article.ignore_comments? }
end

回调类

把回调方法单独放到一个类中。

注意:一般吧回调声明为类方法,这样既有不用实例化对象。

class PictureFileCallbacks
def after_destroy(picture_file)
  if File.exist?(picture_file.filepath)
    File.delete(picture_file.filepath)
  end
end
end
?
class PictureFile < ApplicationRecord
after_destroy PictureFileCallbacks.new
end
?
?
class PictureFileCallbacks
def self.after_destroy(picture_file)
  if File.exist?(picture_file.filepath)
    File.delete(picture_file.filepath)
  end
end
end
?
class PictureFile < ApplicationRecord
after_destroy PictureFileCallbacks
end

可以根据需要在回调类中声明任意多个回调。


事务回调

after_commit和after_rollback这两个回调会在数据库事务完成时触发。

PictureFile模型中的记录删除后,还要删除相应的文件。如果 after_destroy 回调执行后应用引发异常,事务就会回滚,文件会被删除,模型会保持不一致的状态。例如,假设在下面的代码中,picture_file_2对象是无效的,那么调用 save!`方法会引发错误:

PictureFile.transaction do
picture_file_1.destroy
picture_file_2.save!
end
?
?
class PictureFile < ApplicationRecord
after_commit :delete_picture_file_from_disk, on: :destroy

def delete_picture_file_from_disk
  if File.exist?(filepath)
    File.delete(filepath)
  end
end
end

执行回调,保持事务同步。

由于只在执行创建、更新或删除动作时触发 after_commit回调是很常见的,这些操作都拥有别名:

  • after_create_commit

  • after_update_commit

  • after_destroy_commit

    class PictureFile < ApplicationRecord
    after_destroy_commit :delete_picture_file_from_disk

    def delete_picture_file_from_disk
      if File.exist?(filepath)
        File.delete(filepath)
      end
    end
    end

    在事务中创建、更新或删除模型时会调用 after_commitafter_rollback 回调。然而,如果其中有一个回调引发异常,异常会向上冒泡,后续 after_commitafter_rollback 回调不再执行。因此,如果回调代码可能引发异常,就需要在回调中救援并进行适当处理,以便让其他回调继续运行。


    Active Record查询接口

    使用Active Record从数据库中检索数据的不同方法。

    在rails中无需写sql语句,可直接使用封装好的方法。

    find

    查询单个对象
    client=Client.find(10)
    等价于
    SELECT * FROM clients WHERE(client.id=10) LIMIT 1
    ?
    查询多个对象
    client=Client.find([1,10])
    等价于
    SELECT * FROM clients WHERE (client.id IN(1,10))
    如果所提供的主键都没有匹配记录,那么find方法会抛出ActiveRecord::RecordNotFound异常。

    take

    take方法检索一条数据,随机的没有顺序。

    client=Client.take
    等价于
    SELECT * FROM clients LIMIT 1
    如果没有找到记录,返回nil,不抛出异常
    ?
    client=Client.take(2)
    等价于
    SELECT * FROM clients LIMIT 2

    take!方法的行为和take方法类似,区别在于,没找到会抛异常。

    first

    默认查找按主键排序的第一条记录

    client=Client.first
    等价于
    SELECT * FROM clients ORDER BY client.id ASC LIMIT 1
    没找到,返回nil,不会抛异常
    ?
    client=Client.first(3)
    等价于
    SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3
    ?
    也可以按指定的属性进行排列
    client=Client.order(:first_name).first
    等价于
    SELECE * FROM clients ORDER BY clients.first_name ASC LIMIT 1

    first!方法和first类似,区别在于,没找到会抛异常。

    last

    默认查找主键排序的最后一条记录

    client=Client.last
    等价于
    SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
    没找到返回nil,不会抛异常
    ?
    client=Client.last(3)
    SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3
    ?
    client=Client.order(:first_name).last
    SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1

    find_by

    查找匹配指定条件的第一条记录

    Client.find_by first_name:'LIfo'
    Client.where(first_name:'Lifo').take
    SELECT * FROM clients WHERE(clients.first_name='Lifo')LIMIT 1
    ?

    批量检索多个对象

    一次取出所有的数据的话,为每条记录创建模型对象,并把整个模型对象都存入内存中。

    不要一次性保存在内存中,两种方法来处理。

    find_each

    每次检索一批记录,然后逐一吧每条记录作为模型传入块。

    User.find_each do |user|
    NewMailer.weekly(user).deliver_now
    end

    :batch_size

    指定批量处理时,一次检索多少条记录。

    User.find_each(batch_size:5000) do |user|
    NewMailer.weekly(user).deliver_now
    end

    :start :finish

    User.find_each(start:2000,finish:10000) do |user|
    NewMailer.weekly(user).deliver_now
    end

    find_in_batches

    把一批记录作为模型数组传入块。

    Invoice.find_in_batches do |invoices|
    export.add_invoices(invoices)
    end

    条件查询

    Client.where("orders_count=?",params[:orders])
    ?
    Client.where("orders_count=? AND locked=?",params[:orders],false)
    ?
    Client.where("created_at >= :start_date AND creates_at <= :end_date"),
    {start_date:params[:start_date],end_date:params[:end_date]})

    散列条件

    在散列条件中,只能进行相等性、范围和子集检查。

    相等性条件
    Client.where(locked:true)
    Client.where('locked'=>true)
    SELECT * FROM clients WHERE (clients.locked=1)
    ?
    Article.where(author:author)
    Author.joins(:articles).where(articles:{author:author})
    ?
    范围条件
    Client.where(create_at:(Time.now.midnight-1.day)..Time.now.midnight)
    SELECT * FROM clients WHERE(clients,created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
    ?
    子集条件
    用IN表达式来查找记录,在散列条件中使用数组
    Client.where(order_count:[1,3,5])
    SELECE * FROM clients WHERE (clients.order_count IN (1,3,5))

    NOT条件

    Client.where.not(locked:true)
    SELECT * FROM clients WHERE (clients.locked!=1)

    排序

    Client.order("order_count ASC").order("created_at DESC")
    SELECT * FROM clients ORDER BY orders_count ASC,created_at DESC

    选择特定字段

    Client.select("viewable_by,locked")
    Select viewable_by,locked FROM clients
    ?
    去重
    Client.select(:name).distinct
    SELECT DISTINCT name FROM clients

    限量和偏移量

    Client.limit(5)
    SELECT * FROM clients LIMIT 5
    ?
    Client.limit(5).offset(30)
    SELECT * FROM clients LIMIT 5 OFFSET 30

    分组

    Order.select("date(create_at) as order_date,sum(price) as total_price").group("date(create_at)")
    SELECT date(create_at) as order_date,sum(price) as total_price
    FROM orders
    GROUP BY date(create_at)
    ?
    分组项目总数:
    Order.group(:status).count
    SELECT COUNT(*) AS count_all,status AS status
    FROM "order"
    GROUP BY status

    having

    指定分组的约束条件

    Order.select("date(created_at) as ordered_date,sum(price) as total_price").group("date(created_at)").having("sum(price)>?",100)
    ?
    SELECT date(create_at) as ordered_date,sum(price) as total_price
    FROM orders
    GROUP BY date(created_at)
    HAVING sum(price)>100

    条件覆盖

    使用 unscope 方法删除某些条件
    Article.where('id > 10').limit(20).order('id asc').unscope(:order)
    SELECT * FROM articles WHERE id > 10 LIMIT 20
    ?
    使用 only 方法覆盖某些条件
    Article.where('id > 10').limit(20).order('id desc').only(:order, :where)
    SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
    ?
    使用 reorder 方法覆盖默认作用域中的排序方式
    class Article < ApplicationRecord
    has_many :comments, -> { order('posted_at DESC') }
    end
    Article.find(10).comments.reorder('name')
    SELECT * FROM articles WHERE id = 10
    SELECT * FROM comments WHERE article_id = 10 ORDER BY name
    ?
    使用 reverse_order 方法反转排序条件
    Client.where("orders_count > 10").order(:name).reverse_order
    SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC
    ?
    使用 rewhere 方法覆盖 where 方法中指定的条件
    Article.where(trashed: true).rewhere(trashed: false)
    SELECT * FROM articles WHERE `trashed` = 0

    空关系

    none 方法返回可以在链式调用中使用的、不包含任何记录的空关系。在这个空关系上应用后续条件链,会继续生成空关系。对于可能返回零结果、但又需要在链式调用中使用的方法或作用域,可以使用 none 方法来提供返回值。

    Article.none # 返回一个空 Relation 对象,而且不执行查询
    ?
    # 下面的 visible_articles 方法期待返回一个空 Relation 对象
    @articles = current_user.visible_articles.where(name: params[:name])

    def visible_articles
    case role
    when 'Country Manager'
      Article.where(country: country)
    when 'Reviewer'
      Article.published
    when 'Bad User'
      Article.none # => 如果这里返回 [] 或 nil,会导致调用方出错
    end
    end

    只读对象

    在关联中使用 Active Record 提供的 readonly 方法,可以显式禁止修改任何返回对象。如果尝试修改只读对象,不但不会成功,还会抛出 ActiveRecord::ReadOnlyRecord 异常。

    client = Client.readonly.first

    在更新时锁定记录

    Active Record 提供了两种锁定机制:

    • 乐观锁定

    • 悲观锁定

    乐观锁定
    c1 = Client.find(1)
    c2 = Client.find(1)

    c1.first_name = "Michael"
    c1.save

    c2.name = "should fail"
    c2.save # 抛出 ActiveRecord::StaleObjectError
    ?
    ?
    悲观锁定
    在创建关联时使用 lock 方法,会在选定字段上生成互斥锁。使用 lock 方法的关联通常被包装在事务中,以避免发生死锁。
    Item.transaction do
    i = Item.lock.first
    i.name = 'Jones'
    i.save!
    end

    联结表

    Active Record 提供了 joinsleft_outer_joins 这两个查找方法,用于指明生成的 SQL 语句中的 JOIN 子句。其中,joins 方法用于 INNER JOIN 查询或定制查询,left_outer_joins 用于 LEFT OUTER JOIN 查询。

    Joins

    Author.joins("INNER JOIN posts ON posts.author_id=authors.id AND posts.published='t'")
    ?
    SELECT authors.*FROM authors INNER JOIN posts ON posts.author_id=authors.id AND posts.published='t'
    ?
    ?
    class Category < ApplicationRecord
    has_many :articles
    end

    class Article < ApplicationRecord
    belongs_to :category
    has_many :comments
    has_many :tags
    end

    class Comment < ApplicationRecord
    belongs_to :article
    has_one :guest
    end

    class Guest < ApplicationRecord
    belongs_to :comment
    end

    class Tag < ApplicationRecord
    belongs_to :article
    end
    ?
    单个关联的联结
    Category.join(:articles)
    ?
    SELECT categories.* FROM categories
    INNER JOIN articles ON articles.category_id=categories.id

    多个关联的联结
    Article.joins(:category,:comments)
    ?
    SELECT articles.* FROM articles
    INNER JOIN categories ON articles.category_id=categories.id
    INNER JOIN comments ON comments.article_id=articles.id

    单层嵌套关联的联结
    Article.joins(comments: :guest)
    ?
    SELECT articles.* FROM articles
    INNER JOIN comments ON comments.article_id=articles.id
    INNER JOIN guests ON guests.comment_id=comments.id

    多层嵌套关联的联结
    Category.joins(articles:[{comments::guest},:tags])
    ?
    SELECT categories.* FROM categories
    INNER JOIN articles ON articles.category_id=categories.id
    INNER JOIN comments ON comments.article_id=articles.id
    INNER JOIN guests ON guests.comment_id=comments.id
    INNER JOIN tags ON tags.article_id=articles.id


    为联结表指明条件
    time_range = (Time.now.midnight - 1.day)..Time.now.midnight
    Client.joins(:orders).where('orders.created_at' => time_range)
    ?
    time_range = (Time.now.midnight - 1.day)..Time.now.midnight
    Client.joins(:orders).where(orders: { created_at: time_range })
    ?

    left_outer_joins

    选择一组记录,而不管它们是否具有关联记录,可以使用 left_outer_joins 方法。

    Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')
    ?
    SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
    LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id
    返回所有作者和每位作者的帖子数,而不管这些作者是否发过帖子

    及早加载关联

    尽可能减少查询的次数,提高效率。

    N+1查询问题
    clients = Client.limit(10)
    clients.each do |client|
    puts client.address.postcode
    end
    这段代码总共需要执行 1(查找 10 条客户记录)+ 10(每条客户记录都需要加载地址)= 11 次查询
    ?
    ?
    通过指明 includes 方法,Active Record 会使用尽可能少的查询来加载所有已指明的关联
    clients = Client.includes(:address).limit(10)
    clients.each do |client|
    puts client.address.postcode
    end
    SELECT * FROM clients LIMIT 10
    SELECT addresses.* FROM addresses
    WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
    上面的代码只执行 2 次查询,而不是之前的 11 次查询

    作用域

    作用域允许我们把常用查询定义为方法,然后通过在关联对象或模型上调用方法来引用这些查询。fotnote:[“作用域”和“作用域方法”在本文中是一个意思。——译者注]在作用域中,我们可以使用之前介绍过的所有方法,如 wherejoinincludes 方法。所有作用域都会返回 ActiveRecord::Relation 对象,这样就可以继续在这个对象上调用其他方法(如其他作用域)。

    要想定义简单的作用域,我们可以在类中通过 scope 方法定义作用域,并传入调用这个作用域时执行的查询
    class Article < ApplicationRecord
    scope :published, -> { where(published: true) }
    end
    ?
    class Article < ApplicationRecord
    def self.published
      where(published: true)
    end
    end
    ?
    class Article < ApplicationRecord
    scope :published,               -> { where(published: true) }
    scope :published_and_commented, -> { published.where("comments_count > 0") }
    end

    动态查找方法

    Active Record 为数据表中的每个字段(也称为属性)都提供了查找方法(也就是动态查找方法)。例如,对于 Client 模型的 first_name 字段,Active Record 会自动生成 find_by_first_name 查找方法。对于 Client 模型的 locked 字段,Active Record 会自动生成 find_by_locked 查找方法。

    在调用动态查找方法时可以在末尾加上感叹号(!),例如 Client.find_by_name!("Ryan"),这样如果动态查找方法没有返回任何记录,就会抛出 ActiveRecord::RecordNotFound 异常。

    如果想同时查询 first_namelocked 字段,可以在动态查找方法中用 and 把这两个字段连起来,例如 Client.find_by_first_name_and_locked("Ryan", true)

    enum宏

    enum 宏把整数字段映射为一组可能的值

    class Book < ApplicationRecord
    enum availability: [:available, :unavailable]
    end
    ?
    # 下面的示例只查询可用的图书
    Book.available
    # 或
    Book.where(availability: :available)

    book = Book.new(availability: :available)
    book.available?   # => true
    book.unavailable! # => true
    book.available?   # => false

    方法链

    有了方法链我们就可以同时使用多个 Active Record 方法

    Person
    .select('people.id, people.name, comments.text')
    .joins(:comments)
    .where('comments.created_at > ?', 1.week.ago)
    ?
    SELECT people.id, people.name, comments.text
    FROM people
    INNER JOIN comments
    ON comments.person_id = people.id
    WHERE comments.created_at > '2015-01-01'

    查找或创建新对象

    查找记录并在找不到记录时创建记录,这时我们可以使用 find_or_create_byfind_or_create_by! 方法

    Client.find_or_create_by(first_name: 'Andy')
    # => #
    ?
    SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
    BEGIN
    INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
    COMMIT
    ?
    假设我们想在新建记录时把 locked 字段设置为 false,但又不想在查询中进行设置。例如,我们想查找名为“Andy”的客户记录,但这条记录并不存在,因此要创建这条记录并把 locked 字段设置为 false
    Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')
    或者
    Client.find_or_create_by(first_name: 'Andy') do |c|
    c.locked = false
    end

    也可以使用 find_or_create_by! 方法,这样如果新建记录是无效的就会抛出异常。

    find_or_initialize_by 方法的工作原理和 find_or_create_by 方法类似,区别之处在于前者调用的是 new 方法而不是 create 方法。这意味着新建模型实例在内存中创建,但没有保存到数据库。

    使用SQL语句进行查找

    要想直接使用 SQL 语句在数据表中查找记录,可以使用 find_by_sql 方法。find_by_sql 方法总是返回对象的数组,即使底层查询只返回了一条记录也是如此。

    Client.find_by_sql("SELECT * FROM clients
    INNER JOIN orders ON clients.id = orders.client_id
    ORDER BY clients.created_at desc")
    # => [
    #   #,
    #   #,
    #   ...
    # ]

    select_all

    find_by_sql 方法一样,select_all 方法也会使用定制的 SQL 语句从数据库中检索对象,区别在于 select_all 方法不会对这些对象进行实例化,而是返回一个散列构成的数组,其中每个散列表示一条记录。

    Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")
    # => [
    #   {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
    #   {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
    # ]

    pluck方法

    pluck 方法用于在模型对应的底层数据表中查询单个或多个字段。它接受字段名的列表作为参数,并返回这些字段的值的数组,数组中的每个值都具有对应的数据类型。

    Client.where(active: true).pluck(:id)
    # SELECT id FROM clients WHERE active = 1
    # => [1, 2, 3]

    Client.distinct.pluck(:role)
    # SELECT DISTINCT role FROM clients
    # => ['admin', 'member', 'guest']

    Client.pluck(:id, :name)
    # SELECT clients.id, clients.name FROM clients
    # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]

    ids方法

    可以获得关联的所有 ID,也就是数据表的主键。

    Person.ids
    # SELECT id FROM people
    ?
    class Person < ApplicationRecord
    self.primary_key = "person_id"
    end

    Person.ids
    # SELECT person_id FROM people

    检查对象是否存在

    exists? 方法查询数据库的工作原理和 find 方法相同,但是 find 方法返回的是对象或对象集合,而 exists? 方法返回的是 truefalse

    单个值
    Client.exists?(1)
    ?
    多个值
    Client.exists?(id: [1,2,3])
    # 或
    Client.exists?(name: ['John', 'Sergei'])
    并且只要有一条对应记录存在就会返回 true

    我们还可以在模型或关联上调用 any?many? 方法来检查对象是否存在

    # 通过模型
    Article.any?
    Article.many?

    # 通过指定的作用域
    Article.recent.any?
    Article.recent.many?

    # 通过关系
    Article.where(published: true).any?
    Article.where(published: true).many?

    # 通过关联
    Article.first.categories.any?
    Article.first.categories.many?

    计算

    count

    要想知道模型对应的数据表中有多少条记录

    Client.count
    # SELECT count(*) AS count_all FROM clients

    average

    要想知道数据表中某个字段的平均值

    Client.average("orders_count")

    minimum

    要想查找数据表中某个字段的最小值

    Client.minimum("age")

    maximum

    要想查找数据表中某个字段的最大值

    Client.maximum("age")

    sum

    要想知道数据表中某个字段的所有字段值之和

    Client.sum("orders_count")