更多内容请访问 rubyonrails.org:

Active Record 查询接口

本指南介绍了使用 Active Record 从数据库检索数据的不同方法。

阅读完本指南后,您将了解

  • 如何使用各种方法和条件查找记录。
  • 如何指定找到记录的顺序、检索的属性、分组以及其他属性。
  • 如何使用预加载来减少数据检索所需的数据库查询次数。
  • 如何使用动态查找器方法。
  • 如何使用方法链接将多个 Active Record 方法一起使用。
  • 如何检查特定记录是否存在。
  • 如何在 Active Record 模型上执行各种计算。
  • 如何在关系上运行 EXPLAIN。

1 什么是 Active Record 查询接口?

如果您习惯使用原始 SQL 来查找数据库记录,那么通常会发现 Rails 中有更好的方法来执行相同的操作。Active Record 使您免于在大多数情况下使用 SQL 的需要。

Active Record 会为您执行数据库查询,并且与大多数数据库系统兼容,包括 MySQL、MariaDB、PostgreSQL 和 SQLite。无论您使用哪种数据库系统,Active Record 方法格式始终相同。

本指南中的代码示例将引用以下一个或多个模型

所有以下模型都使用id 作为主键,除非另有说明。

class Author < ApplicationRecord
  has_many :books, -> { order(year_published: :desc) }
end
class Book < ApplicationRecord
  belongs_to :supplier
  belongs_to :author
  has_many :reviews
  has_and_belongs_to_many :orders, join_table: "books_orders"

  scope :in_print, -> { where(out_of_print: false) }
  scope :out_of_print, -> { where(out_of_print: true) }
  scope :old, -> { where(year_published: ...50.years.ago.year) }
  scope :out_of_print_and_expensive, -> { out_of_print.where("price > 500") }
  scope :costs_more_than, ->(amount) { where("price > ?", amount) }
end
class Customer < ApplicationRecord
  has_many :orders
  has_many :reviews
end
class Order < ApplicationRecord
  belongs_to :customer
  has_and_belongs_to_many :books, join_table: "books_orders"

  enum :status, [:shipped, :being_packed, :complete, :cancelled]

  scope :created_before, ->(time) { where(created_at: ...time) }
end
class Review < ApplicationRecord
  belongs_to :customer
  belongs_to :book

  enum :state, [:not_reviewed, :published, :hidden]
end
class Supplier < ApplicationRecord
  has_many :books
  has_many :authors, through: :books
end

Diagram of all of the bookstore models

2 从数据库检索对象

要从数据库检索对象,Active Record 提供了几种查找器方法。每种查找器方法允许您将参数传递给它,以便在您的数据库上执行某些查询,而无需编写原始 SQL。

这些方法是

返回集合的查找器方法,例如wheregroup,会返回ActiveRecord::Relation 的实例。查找单个实体的方法,例如findfirst,会返回模型的单个实例。

Model.find(options) 的主要操作可以概括为

  • 将提供的选项转换为等效的 SQL 查询。
  • 执行 SQL 查询并从数据库检索相应的結果。
  • 为每个结果行实例化相应模型的等效 Ruby 对象。
  • 运行after_find,然后运行after_initialize 回调(如果有)。

2.1 检索单个对象

Active Record 提供了几种检索单个对象的不同方法。

2.1.1 find

使用find 方法,您可以检索与指定主键相匹配的任何提供的选项相对应的对象。例如

# Find the customer with primary key (id) 10.
irb> customer = Customer.find(10)
=> #<Customer id: 10, first_name: "Ryan">

上面代码的 SQL 等效代码是

SELECT * FROM customers WHERE (customers.id = 10) LIMIT 1

如果找不到匹配的记录,find 方法将引发ActiveRecord::RecordNotFound 异常。

您还可以使用此方法查询多个对象。调用find 方法并传入一个主键数组。返回值将是一个数组,其中包含与所提供的主键匹配的所有记录。例如

# Find the customers with primary keys 1 and 10.
irb> customers = Customer.find([1, 10]) # OR Customer.find(1, 10)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 10, first_name: "Ryan">]

上面代码的 SQL 等效代码是

SELECT * FROM customers WHERE (customers.id IN (1,10))

find 方法将引发ActiveRecord::RecordNotFound 异常,除非找到与所有提供的首要键匹配的记录。

如果您的表使用组合主键,则需要传递一个数组到 find 以查找单个项目。例如,如果客户定义了[:store_id, :id] 作为主键

# Find the customer with store_id 3 and id 17
irb> customers = Customer.find([3, 17])
=> #<Customer store_id: 3, id: 17, first_name: "Magda">

上面代码的 SQL 等效代码是

SELECT * FROM customers WHERE store_id = 3 AND id = 17

要查找具有组合 ID 的多个客户,您需要传递一个数组的数组

# Find the customers with primary keys [1, 8] and [7, 15].
irb> customers = Customer.find([[1, 8], [7, 15]]) # OR Customer.find([1, 8], [7, 15])
=> [#<Customer store_id: 1, id: 8, first_name: "Pat">, #<Customer store_id: 7, id: 15, first_name: "Chris">]

上面代码的 SQL 等效代码是

SELECT * FROM customers WHERE (store_id = 1 AND id = 8 OR store_id = 7 AND id = 15)

2.1.2 take

take 方法检索记录,不进行任何隐式排序。例如

irb> customer = Customer.take
=> #<Customer id: 1, first_name: "Lifo">

上面代码的 SQL 等效代码是

SELECT * FROM customers LIMIT 1

如果找不到记录,take 方法将返回nil,并且不会引发异常。

您可以将数值参数传递给take 方法以返回最多指定数量的结果。例如

irb> customers = Customer.take(2)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 220, first_name: "Sara">]

上面代码的 SQL 等效代码是

SELECT * FROM customers LIMIT 2

take! 方法的行为与take 完全相同,只是如果找不到匹配的记录,它将引发ActiveRecord::RecordNotFound

检索到的记录可能会因数据库引擎而异。

2.1.3 first

first 方法查找按主键(默认)排序的第一条记录。例如

irb> customer = Customer.first
=> #<Customer id: 1, first_name: "Lifo">

上面代码的 SQL 等效代码是

SELECT * FROM customers ORDER BY customers.id ASC LIMIT 1

如果找不到匹配的记录,first 方法将返回nil,并且不会引发异常。

如果您的默认作用域 包含 order 方法,则first 将返回根据此排序的第一条记录。

您可以将数值参数传递给first 方法以返回最多指定数量的结果。例如

irb> customers = Customer.first(3)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 2, first_name: "Fifo">, #<Customer id: 3, first_name: "Filo">]

上面代码的 SQL 等效代码是

SELECT * FROM customers ORDER BY customers.id ASC LIMIT 3

具有组合主键的模型将使用完整的组合主键进行排序。例如,如果客户定义了[:store_id, :id] 作为主键

irb> customer = Customer.first
=> #<Customer id: 2, store_id: 1, first_name: "Lifo">

上面代码的 SQL 等效代码是

SELECT * FROM customers ORDER BY customers.store_id ASC, customers.id ASC LIMIT 1

在使用order 排序的集合上,first 将返回根据order 指定的属性排序的第一条记录。

irb> customer = Customer.order(:first_name).first
=> #<Customer id: 2, first_name: "Fifo">

上面代码的 SQL 等效代码是

SELECT * FROM customers ORDER BY customers.first_name ASC LIMIT 1

first! 方法的行为与first 完全相同,只是如果找不到匹配的记录,它将引发ActiveRecord::RecordNotFound

2.1.4 last

last 方法查找按主键(默认)排序的最后一条记录。例如

irb> customer = Customer.last
=> #<Customer id: 221, first_name: "Russel">

上面代码的 SQL 等效代码是

SELECT * FROM customers ORDER BY customers.id DESC LIMIT 1

如果找不到匹配的记录,last 方法将返回nil,并且不会引发异常。

具有组合主键的模型将使用完整的组合主键进行排序。例如,如果客户定义了[:store_id, :id] 作为主键

irb> customer = Customer.last
=> #<Customer id: 221, store_id: 1, first_name: "Lifo">

上面代码的 SQL 等效代码是

SELECT * FROM customers ORDER BY customers.store_id DESC, customers.id DESC LIMIT 1

如果您的默认作用域 包含 order 方法,则last 将返回根据此排序的最后一条记录。

您可以将数字参数传递给 last 方法以返回最多该数量的结果。例如

irb> customers = Customer.last(3)
=> [#<Customer id: 219, first_name: "James">, #<Customer id: 220, first_name: "Sara">, #<Customer id: 221, first_name: "Russel">]

上面代码的 SQL 等效代码是

SELECT * FROM customers ORDER BY customers.id DESC LIMIT 3

在使用 order 排序的集合中,last 将返回按 order 指定的属性排序的最后一个记录。

irb> customer = Customer.order(:first_name).last
=> #<Customer id: 220, first_name: "Sara">

上面代码的 SQL 等效代码是

SELECT * FROM customers ORDER BY customers.first_name DESC LIMIT 1

last! 方法的行为与 last 完全相同,只是如果找不到匹配的记录,它将引发 ActiveRecord::RecordNotFound 错误。

2.1.5 find_by

find_by 方法查找与某些条件匹配的第一条记录。例如

irb> Customer.find_by first_name: 'Lifo'
=> #<Customer id: 1, first_name: "Lifo">

irb> Customer.find_by first_name: 'Jon'
=> nil

它等同于编写

Customer.where(first_name: "Lifo").take

上面代码的 SQL 等效代码是

SELECT * FROM customers WHERE (customers.first_name = 'Lifo') LIMIT 1

请注意,上面的 SQL 中没有 ORDER BY。如果您的 find_by 条件可以匹配多条记录,您应该应用排序以保证确定性的结果。

find_by! 方法的行为与 find_by 完全相同,只是如果找不到匹配的记录,它将引发 ActiveRecord::RecordNotFound 错误。例如

irb> Customer.find_by! first_name: 'does not exist'
ActiveRecord::RecordNotFound

这等同于编写

Customer.where(first_name: "does not exist").take!
2.1.5.1 带有 :id 的条件

在为像 find_bywhere 这样的方法指定条件时,使用 id 将匹配模型上的 :id 属性。这与 find 不同,其中传入的 ID 应该是主键值。

在对 :id 不是主键的模型(例如复合主键模型)使用 find_by(id:) 时要小心。例如,如果客户被定义为具有 [:store_id, :id] 作为主键

irb> customer = Customer.last
=> #<Customer id: 10, store_id: 5, first_name: "Joe">
irb> Customer.find_by(id: customer.id) # Customer.find_by(id: [5, 10])
=> #<Customer id: 5, store_id: 3, first_name: "Bob">

在这里,我们可能打算搜索具有复合主键 [5, 10] 的单个记录,但 Active Record 将搜索 :id 列为 5 或 10 的记录,并且可能返回错误的记录。

id_value 方法可用于获取记录的 :id 列的值,用于查找方法(如 find_bywhere)。请参阅下面的示例

irb> customer = Customer.last
=> #<Customer id: 10, store_id: 5, first_name: "Joe">
irb> Customer.find_by(id: customer.id_value) # Customer.find_by(id: 10)
=> #<Customer id: 10, store_id: 5, first_name: "Joe">

2.2 批量检索多个对象

我们经常需要遍历大量记录,就像我们向大量客户发送电子邮件时,或者当我们导出数据时。

这看起来可能很简单

# This may consume too much memory if the table is big.
Customer.all.each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

但随着表大小的增加,这种方法变得越来越不切实际,因为 Customer.all.each 指示 Active Record 在一次传递中获取整个表,为每行构建模型对象,然后将整个模型对象数组保存在内存中。事实上,如果我们有大量的记录,整个集合可能会超过可用内存量。

Rails 提供了两种方法来解决这个问题,它们将记录划分为内存友好的批次进行处理。第一种方法 find_each,检索一批记录,然后将每个记录作为模型逐个传递给块。第二种方法 find_in_batches,检索一批记录,然后将整个批次作为模型数组传递给块。

find_eachfind_in_batches 方法旨在用于对大量无法一次全部放入内存的记录进行批处理。如果您只需要循环遍历一千条记录,则首选使用常规查找方法。

2.2.1 find_each

find_each 方法按批次检索记录,然后将每个记录传递给块。在以下示例中,find_each 按 1000 批次检索客户,并将它们逐个传递给块

Customer.find_each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

此过程会重复进行,根据需要获取更多批次,直到所有记录都被处理。

find_each 在模型类上工作,如上所示,也适用于关系

Customer.where(weekly_subscriber: true).find_each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

只要它们没有排序,因为该方法需要在内部强制排序才能进行迭代。

如果接收器中存在排序,行为将取决于标志config.active_record.error_on_ignored_order。如果为 true,则会引发 ArgumentError,否则将忽略排序并发出警告,这是默认行为。这可以通过选项 :error_on_ignore 覆盖,如下所述。

2.2.1.1 find_each 的选项

:batch_size

:batch_size 选项允许您指定每次批次中要检索的记录数,然后再将其逐个传递给块。例如,要按 5000 批次检索记录

Customer.find_each(batch_size: 5000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

:start

默认情况下,记录按主键的升序获取。:start 选项允许您在最低 ID 不是您需要的 ID 时配置序列的第一个 ID。例如,如果您想恢复中断的批处理进程,前提是您将最后一个处理的 ID 保存为检查点,这将很有用。

例如,要仅向主键从 2000 开始的客户发送电子邮件

Customer.find_each(start: 2000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

:finish

:start 选项类似,:finish 允许您在最高 ID 不是您需要的 ID 时配置序列的最后一个 ID。例如,如果您想使用基于 :start:finish 的记录子集运行批处理进程,这将很有用。

例如,要仅向主键从 2000 开始到 10000 的客户发送电子邮件

Customer.find_each(start: 2000, finish: 10000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

另一个示例是,如果您希望多个工作人员处理同一个处理队列。您可以让每个工作人员处理 10000 条记录,方法是在每个工作人员上设置适当的 :start:finish 选项。

:error_on_ignore

覆盖应用程序配置以指定当关系中存在特定排序时是否应引发错误。

:order

指定主键排序(可以是 :asc:desc)。默认值为 :asc

Customer.find_each(order: :desc) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

2.2.2 find_in_batches

find_in_batches 方法类似于 find_each,因为两者都检索记录批次。区别在于 find_in_batches批次作为模型数组传递给块,而不是逐个传递。以下示例将按一次最多 1000 个客户的数组传递给提供的块,最后一个块包含任何剩余的客户

# Give add_customers an array of 1000 customers at a time.
Customer.find_in_batches do |customers|
  export.add_customers(customers)
end

find_in_batches 在模型类上工作,如上所示,也适用于关系

# Give add_customers an array of 1000 recently active customers at a time.
Customer.recently_active.find_in_batches do |customers|
  export.add_customers(customers)
end

只要它们没有排序,因为该方法需要在内部强制排序才能进行迭代。

2.2.2.1 find_in_batches 的选项

find_in_batches 方法接受与 find_each 相同的选项

:batch_size

find_each 一样,batch_size 建立了每组中要检索的记录数。例如,检索 2500 条记录的批次可以指定为

Customer.find_in_batches(batch_size: 2500) do |customers|
  export.add_customers(customers)
end

:start

start 选项允许指定从何处选择记录的起始 ID。如前所述,默认情况下,记录按主键的升序获取。例如,要从 ID:5000 开始按 2500 条记录的批次检索客户,可以使用以下代码

Customer.find_in_batches(batch_size: 2500, start: 5000) do |customers|
  export.add_customers(customers)
end

:finish

finish 选项允许指定要检索的记录的结束 ID。以下代码显示了按批次检索客户,直到 ID 为 7000 的客户的示例

Customer.find_in_batches(finish: 7000) do |customers|
  export.add_customers(customers)
end

:error_on_ignore

error_on_ignore 选项覆盖应用程序配置以指定当关系中存在特定排序时是否应引发错误。

3 条件

where 方法允许您指定条件来限制返回的记录,表示 SQL 语句的 WHERE 部分。条件可以指定为字符串、数组或哈希。

3.1 纯字符串条件

如果您想在查找中添加条件,您可以直接在其中指定它们,就像 Book.where("title = 'Introduction to Algorithms'") 一样。这将找到所有标题字段值为“Introduction to Algorithms”的书籍。

将您自己的条件构建为纯字符串可能会使您容易受到 SQL 注入攻击。例如,Book.where("title LIKE '%#{params[:title]}%'") 不安全。请参阅下一节,了解使用数组处理条件的首选方法。

3.2 数组条件

现在,如果标题可以变化,例如来自某个地方的参数?然后查找将采用以下形式

Book.where("title = ?", params[:title])

Active Record 将第一个参数作为条件字符串,任何其他参数将替换其中的问号 (?)

如果您想指定多个条件

Book.where("title = ? AND out_of_print = ?", params[:title], false)

在此示例中,第一个问号将替换为 params[:title] 中的值,第二个问号将替换为 false 的 SQL 表示,这取决于适配器。

这段代码非常值得推荐

Book.where("title = ?", params[:title])

这段代码

Book.where("title = #{params[:title]}")

因为参数安全。将变量直接放入条件字符串中将按原样将变量传递给数据库。这意味着它将是来自可能怀有恶意意图的用户的未转义变量。如果您这样做,您将使整个数据库处于风险之中,因为一旦用户发现他们可以利用您的数据库,他们几乎可以对数据库做任何事情。切勿将您的参数直接放入条件字符串中。

有关 SQL 注入危险的更多信息,请参阅Ruby on Rails 安全指南

3.2.1 占位符条件

类似于 (?) 替换样式的参数,您也可以在条件字符串中指定键以及相应的键/值哈希

Book.where("created_at >= :start_date AND created_at <= :end_date",
  { start_date: params[:start_date], end_date: params[:end_date] })

如果您有大量的可变条件,这将提高可读性。

3.2.2 使用 LIKE 的条件

虽然条件参数会自动转义以防止 SQL 注入,但 SQL LIKE 通配符(即 %_不会转义。如果未经消毒的值用于参数,这可能会导致意外行为。例如

Book.where("title LIKE ?", params[:title] + "%")

在上面的代码中,目的是匹配以用户指定字符串开头的标题。但是,params[:title] 中任何出现的 %_ 将被视为通配符,从而导致查询结果令人惊讶。在某些情况下,这也会阻止数据库使用预期的索引,从而导致查询速度大大降低。

为了避免这些问题,请使用sanitize_sql_like 转义参数相关部分中的通配符

Book.where("title LIKE ?",
  Book.sanitize_sql_like(params[:title]) + "%")

3.3 哈希条件

Active Record 还允许您传入哈希条件,这可以提高条件语法的可读性。使用哈希条件,您可以传入一个哈希,其中键是您要限定的字段,值是您要限定它们的方式。

哈希条件仅支持相等、范围和子集检查。

3.3.1 相等条件

Book.where(out_of_print: true)

这将生成以下 SQL:

SELECT * FROM books WHERE (books.out_of_print = 1)

字段名也可以是字符串。

Book.where("out_of_print" => true)

在 belongs_to 关系的情况下,如果使用 Active Record 对象作为值,则可以使用关联键指定模型。此方法也适用于多态关系。

author = Author.first
Book.where(author: author)
Author.joins(:books).where(books: { author: author })

哈希条件也可以在类似元组的语法中指定,其中键是列数组,值是元组数组。

Book.where([:author_id, :id] => [[15, 1], [15, 2]])

此语法对于查询表使用组合主键的关系很有用。

class Book < ApplicationRecord
  self.primary_key = [:author_id, :id]
end

Book.where(Book.primary_key => [[2, 1], [3, 1]])

3.3.2 范围条件

Book.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)

这将通过使用 BETWEEN SQL 语句找到昨天创建的所有书籍。

SELECT * FROM books WHERE (books.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')

这演示了 数组条件 中示例的更短语法。

支持无头和无尾范围,可用于构建小于/大于条件。

Book.where(created_at: (Time.now.midnight - 1.day)..)

这将生成以下 SQL:

SELECT * FROM books WHERE books.created_at >= '2008-12-21 00:00:00'

3.3.3 子集条件

如果您要使用 IN 表达式查找记录,可以将数组传递给条件哈希。

Customer.where(orders_count: [1, 3, 5])

此代码将生成以下 SQL:

SELECT * FROM customers WHERE (customers.orders_count IN (1,3,5))

3.4 NOT 条件

可以通过 where.not 构建 NOT SQL 查询。

Customer.where.not(orders_count: [1, 3, 5])

换句话说,此查询可以通过在没有参数的情况下调用 where,然后立即用 not 传递 where 条件来生成。这将生成以下 SQL:

SELECT * FROM customers WHERE (customers.orders_count NOT IN (1,3,5))

如果查询对可空列具有非空值的哈希条件,则不会返回在可空列上具有 nil 值的记录。例如:

Customer.create!(nullable_country: nil)
Customer.where.not(nullable_country: "UK")
# => []

# But
Customer.create!(nullable_country: "UK")
Customer.where.not(nullable_country: nil)
# => [#<Customer id: 2, nullable_country: "UK">]

3.5 OR 条件

可以通过在第一个关系上调用 or 并将第二个关系作为参数传递来构建两个关系之间的 OR 条件。

Customer.where(last_name: "Smith").or(Customer.where(orders_count: [1, 3, 5]))
SELECT * FROM customers WHERE (customers.last_name = 'Smith' OR customers.orders_count IN (1,3,5))

3.6 AND 条件

可以通过链接 where 条件构建 AND 条件。

Customer.where(last_name: "Smith").where(orders_count: [1, 3, 5])
SELECT * FROM customers WHERE customers.last_name = 'Smith' AND customers.orders_count IN (1,3,5)

可以通过在第一个关系上调用 and 并将第二个关系作为参数传递来构建关系之间逻辑交集的 AND 条件。

Customer.where(id: [1, 2]).and(Customer.where(id: [2, 3]))
SELECT * FROM customers WHERE (customers.id IN (1, 2) AND customers.id IN (2, 3))

4 排序

要按特定顺序从数据库中检索记录,可以使用 order 方法。

例如,如果您正在获取一组记录,并希望按表中 created_at 字段的升序对它们进行排序

Book.order(:created_at)
# OR
Book.order("created_at")

您也可以指定 ASCDESC

Book.order(created_at: :desc)
# OR
Book.order(created_at: :asc)
# OR
Book.order("created_at DESC")
# OR
Book.order("created_at ASC")

或按多个字段排序

Book.order(title: :asc, created_at: :desc)
# OR
Book.order(:title, created_at: :desc)
# OR
Book.order("title ASC, created_at DESC")
# OR
Book.order("title ASC", "created_at DESC")

如果您想多次调用 order,后续的排序将附加到第一个排序。

irb> Book.order("title ASC").order("created_at DESC")
SELECT * FROM books ORDER BY title ASC, created_at DESC

您也可以从联接表中排序

Book.includes(:author).order(books: { print_year: :desc }, authors: { name: :asc })
# OR
Book.includes(:author).order("books.print_year desc", "authors.name asc")

在大多数数据库系统中,在使用诸如 selectpluckids 之类的方法从结果集中选择具有 distinct 的字段时;除非 order 子句中使用的字段包含在选择列表中,否则 order 方法将引发 ActiveRecord::StatementInvalid 异常。有关从结果集中选择字段的信息,请参见下一节。

5 选择特定字段

默认情况下,Model.find 使用 select * 从结果集中选择所有字段。

要从结果集中选择仅一部分字段,可以通过 select 方法指定子集。

例如,要仅选择 isbnout_of_print

Book.select(:isbn, :out_of_print)
# OR
Book.select("isbn, out_of_print")

此查找调用使用的 SQL 查询将类似于:

SELECT isbn, out_of_print FROM books

请注意,这也意味着您仅使用已选择的字段初始化模型对象。如果您尝试访问未在初始化记录中的字段,您将收到

ActiveModel::MissingAttributeError: missing attribute '<attribute>' for Book

其中 <attribute> 是您请求的属性。id 方法不会引发 ActiveRecord::MissingAttributeError,因此在处理关联时要小心,因为它们需要 id 方法才能正常运行。

如果您想只获取某个字段中每个唯一值的一条记录,可以使用 distinct

Customer.select(:last_name).distinct

这将生成以下 SQL:

SELECT DISTINCT last_name FROM customers

您也可以删除唯一性约束

# Returns unique last_names
query = Customer.select(:last_name).distinct

# Returns all last_names, even if there are duplicates
query.distinct(false)

6 限制和偏移

要对 Model.find 触发的 SQL 应用 LIMIT,可以使用 limitoffset 方法在关系上指定 LIMIT

您可以使用 limit 指定要检索的记录数量,并使用 offset 指定在开始返回记录之前要跳过的记录数量。例如

Customer.limit(5)

将返回最多 5 个客户,因为它没有指定偏移量,因此将返回表中的前 5 个。它执行的 SQL 如下所示

SELECT * FROM customers LIMIT 5

在其中添加 offset

Customer.limit(5).offset(30)

将改为返回从第 31 个开始的最多 5 个客户。SQL 如下所示

SELECT * FROM customers LIMIT 5 OFFSET 30

7 分组

要对查找器触发的 SQL 应用 GROUP BY 子句,可以使用 group 方法。

例如,如果您想查找创建订单的日期集合

Order.select("created_at").group("created_at")

这将为您提供每个日期的一个 Order 对象,其中数据库中有订单。

将执行的 SQL 将类似于:

SELECT created_at
FROM orders
GROUP BY created_at

7.1 分组项总数

要在单个查询上获取分组项的总数,请在 group 之后调用 count

irb> Order.group(:status).count
=> {"being_packed"=>7, "shipped"=>12}

将执行的 SQL 将类似于:

SELECT COUNT (*) AS count_all, status AS status
FROM orders
GROUP BY status

7.2 HAVING 条件

SQL 使用 HAVING 子句指定对 GROUP BY 字段的条件。您可以通过在查找中添加 having 方法,将 HAVING 子句添加到 Model.find 触发的 SQL 中。

例如

Order.select("created_at as ordered_date, sum(total) as total_price").
  group("created_at").having("sum(total) > ?", 200)

将执行的 SQL 将类似于:

SELECT created_at as ordered_date, sum(total) as total_price
FROM orders
GROUP BY created_at
HAVING sum(total) > 200

这将返回每个订单对象的日期和总价,按它们下订单的日期分组,并且总价大于 $200。

您可以像这样访问返回的每个订单对象的 total_price

big_orders = Order.select("created_at, sum(total) as total_price")
                  .group("created_at")
                  .having("sum(total) > ?", 200)

big_orders[0].total_price
# Returns the total price for the first Order object

8 覆盖条件

8.1 unscope

您可以使用 unscope 方法指定要删除的某些条件。例如

Book.where("id > 100").limit(20).order("id desc").unscope(:order)

将执行的 SQL

SELECT * FROM books WHERE id > 100 LIMIT 20

-- Original query without `unscope`
SELECT * FROM books WHERE id > 100 ORDER BY id desc LIMIT 20

您也可以取消范围特定的 where 子句。例如,这将从 where 子句中删除 id 条件

Book.where(id: 10, out_of_print: false).unscope(where: :id)
# SELECT books.* FROM books WHERE out_of_print = 0

已使用 unscope 的关系将影响合并到其中的任何关系

Book.order("id desc").merge(Book.unscope(:order))
# SELECT books.* FROM books

8.2 only

您也可以使用 only 方法覆盖条件。例如

Book.where("id > 10").limit(20).order("id desc").only(:order, :where)

将执行的 SQL

SELECT * FROM books WHERE id > 10 ORDER BY id DESC

-- Original query without `only`
SELECT * FROM books WHERE id > 10 ORDER BY id DESC LIMIT 20

8.3 reselect

reselect 方法覆盖现有的 select 语句。例如

Book.select(:title, :isbn).reselect(:created_at)

将执行的 SQL

SELECT books.created_at FROM books

将其与未使用 reselect 子句的情况进行比较

Book.select(:title, :isbn).select(:created_at)

执行的 SQL 将是

SELECT books.title, books.isbn, books.created_at FROM books

8.4 reorder

reorder 方法覆盖默认范围顺序。例如,如果类定义包含以下内容

class Author < ApplicationRecord
  has_many :books, -> { order(year_published: :desc) }
end

并且您执行以下操作

Author.find(10).books

将执行的 SQL

SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published DESC

您可以使用 reorder 子句指定对书籍进行排序的不同方式

Author.find(10).books.reorder("year_published ASC")

将执行的 SQL

SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published ASC

8.5 reverse_order

reverse_order 方法反转排序子句(如果指定)。

Book.where("author_id > 10").order(:year_published).reverse_order

将执行的 SQL

SELECT * FROM books WHERE author_id > 10 ORDER BY year_published DESC

如果查询中未指定排序子句,reverse_order 将按主键的逆序进行排序。

Book.where("author_id > 10").reverse_order

将执行的 SQL

SELECT * FROM books WHERE author_id > 10 ORDER BY books.id DESC

reverse_order 方法不接受任何参数。

8.6 rewhere

rewhere 方法覆盖现有的命名 where 条件。例如

Book.where(out_of_print: true).rewhere(out_of_print: false)

将执行的 SQL

SELECT * FROM books WHERE out_of_print = 0

如果没有使用 rewhere 子句,则 where 子句将用 AND 连接在一起

Book.where(out_of_print: true).where(out_of_print: false)

执行的 SQL 将是

SELECT * FROM books WHERE out_of_print = 1 AND out_of_print = 0

8.7 regroup

regroup 方法覆盖现有的命名 group 条件。例如

Book.group(:author).regroup(:id)

将执行的 SQL

SELECT * FROM books GROUP BY id

如果没有使用 regroup 子句,则 group 子句将组合在一起

Book.group(:author).group(:id)

执行的 SQL 将是

SELECT * FROM books GROUP BY author, id

9 空关系

none 方法返回一个没有记录的可链接关系。链接到返回关系的任何后续条件将继续生成空关系。这在您需要对可能返回零结果的方法或范围进行可链接响应的情况下很有用。

Book.none # returns an empty Relation and fires no queries.
# The highlighted_reviews method below is expected to always return a Relation.
Book.first.highlighted_reviews.average(:rating)
# => Returns average rating of a book

class Book
  # Returns reviews if there are at least 5,
  # else consider this as non-reviewed book
  def highlighted_reviews
    if reviews.count > 5
      reviews
    else
      Review.none # Does not meet minimum threshold yet
    end
  end
end

10 只读对象

Active Record 在关系上提供了 readonly 方法,以显式地禁止修改任何返回的对象。任何尝试更改只读记录的操作都将失败,并引发 ActiveRecord::ReadOnlyRecord 异常。

customer = Customer.readonly.first
customer.visits += 1
customer.save # Raises an ActiveRecord::ReadOnlyRecord

由于 customer 被显式地设置为只读对象,因此当使用更新后的 visits 值调用 customer.save 时,上面的代码将引发 ActiveRecord::ReadOnlyRecord 异常。

11 锁定记录以进行更新

锁定在更新数据库中的记录时防止竞争条件并确保原子更新方面很有帮助。

Active Record 提供了两种锁定机制

  • 乐观锁定
  • 悲观锁定

11.1 乐观锁定

乐观锁定允许多个用户访问同一记录进行编辑,并假设与数据的冲突最少。它通过检查自记录打开以来是否有其他进程对其进行了更改来做到这一点。如果发生这种情况,将引发 ActiveRecord::StaleObjectError 异常,并且更新将被忽略。

乐观锁定列

为了使用乐观锁定,表需要有一个名为 lock_version 的列,其类型为整数。每次更新记录时,Active Record 都会递增 lock_version 列。如果更新请求使用的 lock_version 字段中的值低于数据库中 lock_version 列中的当前值,则更新请求将失败,并引发 ActiveRecord::StaleObjectError

例如

c1 = Customer.find(1)
c2 = Customer.find(1)

c1.first_name = "Sandra"
c1.save

c2.first_name = "Michael"
c2.save # Raises an ActiveRecord::StaleObjectError

然后您负责处理冲突,方法是捕获异常,并回滚、合并或以其他方式应用必要的业务逻辑来解决冲突。

可以通过设置 ActiveRecord::Base.lock_optimistically = false 来关闭此行为。

要覆盖 lock_version 列的名称,ActiveRecord::Base 提供了一个名为 locking_column 的类属性。

class Customer < ApplicationRecord
  self.locking_column = :lock_customer_column
end

11.2 悲观锁定

悲观锁定使用底层数据库提供的锁定机制。在构建关系时使用 lock 会获取所选行的独占锁。使用 lock 的关系通常包装在事务中以防止死锁条件。

例如

Book.transaction do
  book = Book.lock.first
  book.title = "Algorithms, second edition"
  book.save!
end

上面的会话为 MySQL 后端生成以下 SQL

SQL (0.2ms)   BEGIN
Book Load (0.3ms)   SELECT * FROM books LIMIT 1 FOR UPDATE
Book Update (0.4ms)   UPDATE books SET updated_at = '2009-02-07 18:05:56', title = 'Algorithms, second edition' WHERE id = 1
SQL (0.8ms)   COMMIT

您还可以将原始 SQL 传递给 lock 方法以允许不同类型的锁。例如,MySQL 有一个名为 LOCK IN SHARE MODE 的表达式,您可以锁定记录,但仍然允许其他查询读取它。要指定此表达式,只需将其作为锁选项传递即可

Book.transaction do
  book = Book.lock("LOCK IN SHARE MODE").find(1)
  book.increment!(:views)
end

请注意,您的数据库必须支持您传递给 lock 方法的原始 SQL。

如果您已经拥有模型的实例,则可以使用以下代码启动事务并立即获取锁

book = Book.first
book.with_lock do
  # This block is called within a transaction,
  # book is already locked.
  book.increment!(:views)
end

12 连接表

Active Record 提供了两种查找方法来指定结果 SQL 上的 JOIN 子句:joinsleft_outer_joins。虽然 joins 应该用于 INNER JOIN 或自定义查询,但 left_outer_joins 用于使用 LEFT OUTER JOIN 的查询。

12.1 joins

有多种方法可以使用 joins 方法。

12.1.1 使用字符串 SQL 片段

您只需向 joins 提供指定 JOIN 子句的原始 SQL 即可

Author.joins("INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE")

这将产生以下 SQL

SELECT authors.* FROM authors INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE

12.1.2 使用命名关联的数组/哈希

Active Record 允许您使用模型上定义的 关联 的名称作为快捷方式,用于在使用 joins 方法时为这些关联指定 JOIN 子句。

以下所有操作都将使用 INNER JOIN 生成预期的连接查询

12.1.2.1 连接单个关联
Book.joins(:reviews)

这会产生

SELECT books.* FROM books
  INNER JOIN reviews ON reviews.book_id = books.id

或者,用英语:“返回所有带有评论的书籍的 Book 对象”。请注意,如果一本书有多个评论,您会看到重复的书籍。如果您想要唯一的书籍,可以使用 Book.joins(:reviews).distinct

12.1.3 连接多个关联

Book.joins(:author, :reviews)

这会产生

SELECT books.* FROM books
  INNER JOIN authors ON authors.id = books.author_id
  INNER JOIN reviews ON reviews.book_id = books.id

或者,用英语:“返回所有具有作者和至少一条评论的书籍”。请再次注意,具有多个评论的书籍将显示多次。

12.1.3.1 连接嵌套关联(单级)
Book.joins(reviews: :customer)

这会产生

SELECT books.* FROM books
  INNER JOIN reviews ON reviews.book_id = books.id
  INNER JOIN customers ON customers.id = reviews.customer_id

或者,用英语:“返回所有由客户评论的书籍。”

12.1.3.2 连接嵌套关联(多级)
Author.joins(books: [{ reviews: { customer: :orders } }, :supplier])

这会产生

SELECT authors.* FROM authors
  INNER JOIN books ON books.author_id = authors.id
  INNER JOIN reviews ON reviews.book_id = books.id
  INNER JOIN customers ON customers.id = reviews.customer_id
  INNER JOIN orders ON orders.customer_id = customers.id
INNER JOIN suppliers ON suppliers.id = books.supplier_id

或者,用英语:“返回所有具有带有评论的书籍并且已由客户订购的作者,以及这些书籍的供应商。”

12.1.4 指定连接表上的条件

您可以使用常规的 数组字符串 条件来指定连接表上的条件。 哈希条件 提供了一种特殊的语法来指定连接表的条件

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where("orders.created_at" => time_range).distinct

这将找到所有在昨天创建订单的客户,使用 BETWEEN SQL 表达式来比较 created_at

另一种更简洁的语法是嵌套哈希条件

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where(orders: { created_at: time_range }).distinct

对于更高级的条件或重用现有命名范围,可以使用 merge。首先,让我们在 Order 模型中添加一个新的命名范围

class Order < ApplicationRecord
  belongs_to :customer

  scope :created_in_time_range, ->(time_range) {
    where(created_at: time_range)
  }
end

现在我们可以使用 mergecreated_in_time_range 范围合并进来

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).merge(Order.created_in_time_range(time_range)).distinct

这将找到所有在昨天创建订单的客户,再次使用 BETWEEN SQL 表达式。

12.2 left_outer_joins

如果您想选择一组记录,无论它们是否有关联记录,都可以使用 left_outer_joins 方法。

Customer.left_outer_joins(:reviews).distinct.select("customers.*, COUNT(reviews.*) AS reviews_count").group("customers.id")

这会产生

SELECT DISTINCT customers.*, COUNT(reviews.*) AS reviews_count FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id GROUP BY customers.id

这意味着:“返回所有客户及其评论数量,无论他们是否有任何评论。”

12.3 where.associatedwhere.missing

associatedmissing 查询方法允许您根据关联的存在或不存在来选择一组记录。

要使用 where.associated

Customer.where.associated(:reviews)

产生

SELECT customers.* FROM customers
INNER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NOT NULL

这意味着“返回所有至少进行过一次评论的客户”。

要使用 where.missing

Customer.where.missing(:reviews)

产生

SELECT customers.* FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NULL

这意味着“返回所有没有进行过任何评论的客户”。

13 渴望加载关联

渴望加载是使用尽可能少的查询来加载 Model.find 返回的对象的关联记录的机制。

13.1 N + 1 查询问题

考虑以下代码,它查找 10 本书并打印其作者的 last_name

books = Book.limit(10)

books.each do |book|
  puts book.author.last_name
end

乍一看,这段代码看起来不错。但问题在于执行的总查询数。上面的代码总共执行 1(查找 10 本书)+ 10(每本书加载一个作者)= **11** 个查询。

13.1.1 N + 1 查询问题的解决方案

Active Record 允许您提前指定要加载的所有关联。

这些方法是

13.2 includes

使用 includes,Active Record 确保使用尽可能少的查询加载所有指定的关联。

重新审视上面使用 includes 方法的案例,我们可以重写 Book.limit(10) 来渴望加载作者

books = Book.includes(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

上面的代码将仅执行 **2** 个查询,而不是原始情况下的 **11** 个查询

SELECT books.* FROM books LIMIT 10
SELECT authors.* FROM authors
  WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)

13.2.1 渴望加载多个关联

Active Record 允许您使用 includes 方法的数组、哈希或嵌套的数组/哈希来加载多个关联,并使用单个 Model.find 调用。

13.2.1.1 多个关联的数组
Customer.includes(:orders, :reviews)

这将加载所有客户以及每个客户的关联订单和评论。

13.2.1.2 嵌套关联哈希
Customer.includes(orders: { books: [:supplier, :author] }).find(1)

这将找到 id 为 1 的客户,并渴望加载与其关联的所有订单,所有订单的书籍,以及每本书的作者和供应商。

13.2.2 指定渴望加载关联的条件

即使 Active Record 允许您像 joins 一样指定渴望加载关联的条件,但建议的方法是使用 joins

但是,如果您必须这样做,您可以像往常一样使用 where

Author.includes(:books).where(books: { out_of_print: true })

这将生成一个包含 LEFT OUTER JOIN 的查询,而 joins 方法将使用 INNER JOIN 函数生成一个查询。

  SELECT authors.id AS t0_r0, ... books.updated_at AS t1_r5 FROM authors LEFT OUTER JOIN books ON books.author_id = authors.id WHERE (books.out_of_print = 1)

如果没有 where 条件,这将生成正常的两组查询。

仅当您向其传递一个哈希时,此处的 where 才会起作用。对于 SQL 片段,您需要使用 references 来强制连接表

Author.includes(:books).where("books.out_of_print = true").references(:books)

在本 includes 查询的情况下,如果任何作者都没有书籍,所有作者仍将被加载。通过使用 joins(INNER JOIN),连接条件**必须**匹配,否则不会返回任何记录。

如果关联是作为连接的一部分渴望加载的,则加载的模型上不会出现来自自定义 select 子句的任何字段。这是因为它不清楚它们应该出现在父记录上还是子记录上。

13.3 preload

使用 preload,Active Record 使用每个关联一个查询来加载每个指定的关联。

重新审视 N + 1 查询问题,我们可以重写 Book.limit(10) 来预加载作者

books = Book.preload(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

上面的代码将仅执行 **2** 个查询,而不是原始情况下的 **11** 个查询

SELECT books.* FROM books LIMIT 10
SELECT authors.* FROM authors
  WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)

preload 方法与 includes 方法相同,使用数组、哈希或嵌套的数组/哈希来使用单个 Model.find 调用加载任意数量的关联。但是,与 includes 方法不同,无法为预加载的关联指定条件。

13.4 eager_load

使用 eager_load,Active Record 使用 LEFT OUTER JOIN 加载所有指定的关联。

重新审视使用 eager_load 方法发生 N + 1 的情况,我们可以重写 Book.limit(10) 到作者

books = Book.eager_load(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

上面的代码将仅执行 **1** 个查询,而不是原始情况下的 **11** 个查询

SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, ... FROM "books"
  LEFT OUTER JOIN "authors" ON "authors"."id" = "books"."author_id"
  LIMIT 10

eager_load 方法与 includes 方法相同,使用数组、哈希或嵌套的数组/哈希来使用单个 Model.find 调用加载任意数量的关联。此外,与 includes 方法一样,您可以指定渴望加载关联的条件。

13.5 strict_loading

渴望加载可以防止 N + 1 查询,但您可能仍然在懒加载某些关联。为了确保没有关联被懒加载,您可以启用 strict_loading

通过在关系上启用严格加载模式,如果记录尝试懒加载任何关联,将引发 ActiveRecord::StrictLoadingViolationError

user = User.strict_loading.first
user.address.city # raises an ActiveRecord::StrictLoadingViolationError
user.comments.to_a # raises an ActiveRecord::StrictLoadingViolationError

要为所有关系启用,请将 config.active_record.strict_loading_by_default 标志更改为 true

要将违规发送到日志记录器,请将 config.active_record.action_on_strict_loading_violation 更改为 :log

13.6 strict_loading!

我们还可以通过调用 strict_loading! 来在记录本身启用严格加载

user = User.first
user.strict_loading!
user.address.city # raises an ActiveRecord::StrictLoadingViolationError
user.comments.to_a # raises an ActiveRecord::StrictLoadingViolationError

strict_loading! 还接受一个 :mode 参数。将其设置为 :n_plus_one_only 将仅在懒加载会导致 N + 1 查询的关联时引发错误

user.strict_loading!(mode: :n_plus_one_only)
user.address.city # => "Tatooine"
user.comments.to_a # => [#<Comment:0x00...]
user.comments.first.likes.to_a # raises an ActiveRecord::StrictLoadingViolationError

13.7 关联上的 strict_loading 选项

我们还可以通过提供 strict_loading 选项来为单个关联启用严格加载

class Author < ApplicationRecord
  has_many :books, strict_loading: true
end

14 范围

范围允许您指定常用查询,这些查询可以作为关联对象或模型上的方法调用进行引用。使用这些范围,您可以使用之前介绍的每种方法,例如 wherejoinsincludes。所有范围主体都应返回一个 ActiveRecord::Relationnil,以允许在它上面调用进一步的方法(例如其他范围)。

要定义一个简单的范围,我们在类内部使用 scope 方法,传入我们想要在调用此范围时运行的查询

class Book < ApplicationRecord
  scope :out_of_print, -> { where(out_of_print: true) }
end

要调用此 out_of_print 范围,我们可以在类上调用它

irb> Book.out_of_print
=> #<ActiveRecord::Relation> # all out of print books

或者在包含 Book 对象的关联上调用它

irb> author = Author.first
irb> author.books.out_of_print
=> #<ActiveRecord::Relation> # all out of print books by `author`

范围也可以在范围内部进行链接

class Book < ApplicationRecord
  scope :out_of_print, -> { where(out_of_print: true) }
  scope :out_of_print_and_expensive, -> { out_of_print.where("price > 500") }
end

14.1 传递参数

您的范围可以接受参数

class Book < ApplicationRecord
  scope :costs_more_than, ->(amount) { where("price > ?", amount) }
end

调用范围就像调用类方法一样

irb> Book.costs_more_than(100.10)

但是,这只是在重复类方法为您提供的功能。

class Book < ApplicationRecord
  def self.costs_more_than(amount)
    where("price > ?", amount)
  end
end

这些方法仍然可以在关联对象上访问

irb> author.books.costs_more_than(100.10)

14.2 使用条件语句

您的范围可以使用条件语句

class Order < ApplicationRecord
  scope :created_before, ->(time) { where(created_at: ...time) if time.present? }
end

与其他示例类似,这将与类方法类似。

class Order < ApplicationRecord
  def self.created_before(time)
    where(created_at: ...time) if time.present?
  end
end

但是,有一个重要的警告:即使条件评估为false,作用域始终会返回一个ActiveRecord::Relation对象,而类方法将返回nil。如果任何条件返回false,这会导致在将条件与类方法链接时出现NoMethodError

14.3 应用默认作用域

如果我们希望作用域应用于模型的所有查询,我们可以在模型本身中使用default_scope方法。

class Book < ApplicationRecord
  default_scope { where(out_of_print: false) }
end

当对该模型执行查询时,SQL 查询现在将类似于以下内容

SELECT * FROM books WHERE (out_of_print = false)

如果您需要使用默认作用域执行更复杂的操作,您可以选择将其定义为类方法

class Book < ApplicationRecord
  def self.default_scope
    # Should return an ActiveRecord::Relation.
  end
end

当作用域参数以Hash形式给出时,default_scope也会在创建/构建记录时应用。它不会在更新记录时应用。例如:

class Book < ApplicationRecord
  default_scope { where(out_of_print: false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: false>
irb> Book.unscoped.new
=> #<Book id: nil, out_of_print: nil>

请注意,当以Array格式给出时,default_scope查询参数无法转换为Hash以用于默认属性赋值。例如:

class Book < ApplicationRecord
  default_scope { where("out_of_print = ?", false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: nil>

14.4 作用域的合并

就像where子句一样,作用域使用AND条件合并。

class Book < ApplicationRecord
  scope :in_print, -> { where(out_of_print: false) }
  scope :out_of_print, -> { where(out_of_print: true) }

  scope :recent, -> { where(year_published: 50.years.ago.year..) }
  scope :old, -> { where(year_published: ...50.years.ago.year) }
end
irb> Book.out_of_print.old
SELECT books.* FROM books WHERE books.out_of_print = 'true' AND books.year_published < 1969

我们可以混合使用scopewhere条件,最终的 SQL 将以AND连接所有条件。

irb> Book.in_print.where(price: ...100)
SELECT books.* FROM books WHERE books.out_of_print = 'false' AND books.price < 100

如果我们确实希望最后一个where子句生效,则可以使用merge

irb> Book.in_print.merge(Book.out_of_print)
SELECT books.* FROM books WHERE books.out_of_print = true

一个重要的警告是,default_scope将预先添加到scopewhere条件中。

class Book < ApplicationRecord
  default_scope { where(year_published: 50.years.ago.year..) }

  scope :in_print, -> { where(out_of_print: false) }
  scope :out_of_print, -> { where(out_of_print: true) }
end
irb> Book.all
SELECT books.* FROM books WHERE (year_published >= 1969)

irb> Book.in_print
SELECT books.* FROM books WHERE (year_published >= 1969) AND books.out_of_print = false

irb> Book.where('price > 50')
SELECT books.* FROM books WHERE (year_published >= 1969) AND (price > 50)

如您在上面所见,default_scope正在scopewhere条件中合并。

14.5 移除所有作用域

如果我们希望出于任何原因移除作用域,我们可以使用unscoped方法。这在模型中指定了default_scope并且不应该为此特定查询应用时尤其有用。

Book.unscoped.load

此方法将移除所有作用域,并对表执行正常查询。

irb> Book.unscoped.all
SELECT books.* FROM books

irb> Book.where(out_of_print: true).unscoped.all
SELECT books.* FROM books

unscoped也可以接受一个块

irb> Book.unscoped { Book.out_of_print }
SELECT books.* FROM books WHERE books.out_of_print = true

15 动态查找器

对于您在表中定义的每个字段(也称为属性),Active Record 都提供了一个查找器方法。例如,如果您在Customer模型上有一个名为first_name的字段,那么您可以免费从 Active Record 获取find_by_first_name实例方法。如果您在Customer模型上也拥有locked字段,那么您也将获得find_by_locked方法。

您可以在动态查找器的末尾指定一个感叹号(!),以便在它们没有返回任何记录时引发ActiveRecord::RecordNotFound错误,例如Customer.find_by_first_name!("Ryan")

如果您想按first_nameorders_count同时查找,您可以通过在字段之间简单地键入“and”来将这些查找器链接在一起。例如,Customer.find_by_first_name_and_orders_count("Ryan", 5)

16 枚举

枚举允许您为属性定义一个值数组,并通过名称引用它们。存储在数据库中的实际值是一个整数,它已被映射到其中一个值。

声明枚举将

  • 创建作用域,可用于查找所有拥有或不拥有枚举值之一的对象
  • 创建一个实例方法,可用于确定对象是否具有枚举的特定值
  • 创建一个实例方法,可用于更改对象的枚举值

用于枚举的所有可能值。

例如,给定此enum声明

class Order < ApplicationRecord
  enum :status, [:shipped, :being_packaged, :complete, :cancelled]
end

这些作用域是自动创建的,可用于查找具有或不具有status的特定值的所以对象

irb> Order.shipped
=> #<ActiveRecord::Relation> # all orders with status == :shipped
irb> Order.not_shipped
=> #<ActiveRecord::Relation> # all orders with status != :shipped

这些实例方法是自动创建的,并查询模型是否对status枚举具有该值

irb> order = Order.shipped.first
irb> order.shipped?
=> true
irb> order.complete?
=> false

这些实例方法是自动创建的,它们将首先将status的值更新为命名值,然后查询是否已将状态成功设置为该值

irb> order = Order.first
irb> order.shipped!
UPDATE "orders" SET "status" = ?, "updated_at" = ? WHERE "orders"."id" = ?  [["status", 0], ["updated_at", "2019-01-24 07:13:08.524320"], ["id", 1]]
=> true

有关枚举的完整文档,请参阅此处

17 理解方法链接

Active Record 模式实现了方法链接,这使我们能够以简单直观的方式将多个 Active Record 方法组合使用。

当先前调用的方法返回一个ActiveRecord::Relation(如allwherejoins)时,您可以在语句中链接方法。返回单个对象的方法(请参阅检索单个对象部分)必须位于语句的末尾。

下面有一些示例。本指南不会涵盖所有可能性,仅作为一些示例。当调用 Active Record 方法时,查询不会立即生成并发送到数据库。查询仅在实际需要数据时发送。因此,下面的每个示例都生成一个查询。

17.1 从多个表中检索已过滤的数据

Customer
  .select("customers.id, customers.last_name, reviews.body")
  .joins(:reviews)
  .where("reviews.created_at > ?", 1.week.ago)

结果应类似于以下内容

SELECT customers.id, customers.last_name, reviews.body
FROM customers
INNER JOIN reviews
  ON reviews.customer_id = customers.id
WHERE (reviews.created_at > '2019-01-08')

17.2 从多个表中检索特定数据

Book
  .select("books.id, books.title, authors.first_name")
  .joins(:author)
  .find_by(title: "Abstraction and Specification in Program Development")

以上应生成

SELECT books.id, books.title, authors.first_name
FROM books
INNER JOIN authors
  ON authors.id = books.author_id
WHERE books.title = $1 [["title", "Abstraction and Specification in Program Development"]]
LIMIT 1

请注意,如果查询匹配多个记录,find_by只会获取第一个记录,而忽略其他记录(请参阅上面的LIMIT 1语句)。

18 查找或构建新对象

您通常需要查找记录,如果不存在则创建它。您可以使用find_or_create_byfind_or_create_by!方法来完成此操作。

18.1 find_or_create_by

find_or_create_by方法检查具有指定属性的记录是否存在。如果不存在,则调用create。让我们看一个例子。

假设您想查找名为“Andy”的客户,如果不存在,则创建一个。您可以通过运行以下命令来做到这一点

irb> Customer.find_or_create_by(first_name: 'Andy')
=> #<Customer id: 5, first_name: "Andy", last_name: nil, title: nil, visits: 0, orders_count: nil, lock_version: 0, created_at: "2019-01-17 07:06:45", updated_at: "2019-01-17 07:06:45">

此方法生成的 SQL 如下所示

SELECT * FROM customers WHERE (customers.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO customers (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

find_or_create_by返回已经存在的记录或新记录。在我们的例子中,我们还没有名为 Andy 的客户,因此记录被创建并返回。

新记录可能不会保存到数据库中;这取决于验证是否通过(就像create一样)。

假设我们想在创建新记录时将“locked”属性设置为false,但我们不想将其包含在查询中。因此,我们想查找名为“Andy”的客户,或者如果该客户不存在,则创建一个名为“Andy”且未锁定的客户。

我们可以通过两种方式实现。第一种是使用create_with

Customer.create_with(locked: false).find_or_create_by(first_name: "Andy")

第二种方法是使用一个块

Customer.find_or_create_by(first_name: "Andy") do |c|
  c.locked = false
end

该块仅在创建客户时执行。当我们第二次运行此代码时,该块将被忽略。

18.2 find_or_create_by!

您也可以使用find_or_create_by!,如果新记录无效则引发异常。验证在本指南中没有涵盖,但让我们假设您暂时将

validates :orders_count, presence: true

添加到您的Customer模型中。如果您尝试在没有传递orders_count的情况下创建新的Customer,则该记录将无效,并且会引发异常

irb> Customer.find_or_create_by!(first_name: 'Andy')
ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank

18.3 find_or_initialize_by

find_or_initialize_by方法的工作方式与find_or_create_by类似,但它将调用new而不是create。这意味着将在内存中创建一个新的模型实例,但不会保存到数据库中。继续使用find_or_create_by示例,我们现在想要名为“Nina”的客户

irb> nina = Customer.find_or_initialize_by(first_name: 'Nina')
=> #<Customer id: nil, first_name: "Nina", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">

irb> nina.persisted?
=> false

irb> nina.new_record?
=> true

因为该对象尚未存储在数据库中,因此生成的 SQL 如下所示

SELECT * FROM customers WHERE (customers.first_name = 'Nina') LIMIT 1

当您想将其保存到数据库时,只需调用save

irb> nina.save
=> true

19 按 SQL 查找

如果您想使用自己的 SQL 在表中查找记录,可以使用find_by_sqlfind_by_sql方法将返回一个对象数组,即使底层查询只返回单个记录也是如此。例如,您可以运行此查询

irb> Customer.find_by_sql("SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id ORDER BY customers.created_at desc")
=> [#<Customer id: 1, first_name: "Lucas" ...>, #<Customer id: 2, first_name: "Jan" ...>, ...]

find_by_sql为您提供了一种简单的方法来对数据库进行自定义调用并检索实例化的对象。

19.1 select_all

find_by_sql有一个名为lease_connection.select_all的近亲。select_all将使用自定义 SQL 从数据库中检索对象,就像find_by_sql一样,但不会实例化它们。此方法将返回ActiveRecord::Result类的实例,对该对象调用to_a将返回一个哈希数组,其中每个哈希表示一条记录。

irb> Customer.lease_connection.select_all("SELECT first_name, created_at FROM customers WHERE id = '1'").to_a
=> [{"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}]

19.2 pluck

pluck可用于从当前关系中指定的列中挑选值。它接受列名列表作为参数,并返回一个包含指定列值的数组,以及相应的类型。

irb> Book.where(out_of_print: true).pluck(:id)
SELECT id FROM books WHERE out_of_print = true
=> [1, 2, 3]

irb> Order.distinct.pluck(:status)
SELECT DISTINCT status FROM orders
=> ["shipped", "being_packed", "cancelled"]

irb> Customer.pluck(:id, :first_name)
SELECT customers.id, customers.first_name FROM customers
=> [[1, "David"], [2, "Fran"], [3, "Jose"]]

pluck使替换以下代码成为可能

Customer.select(:id).map { |c| c.id }
# or
Customer.select(:id).map(&:id)
# or
Customer.select(:id, :first_name).map { |c| [c.id, c.first_name] }

使用

Customer.pluck(:id)
# or
Customer.pluck(:id, :first_name)

select不同,pluck直接将数据库结果转换为 Ruby Array,而无需构造ActiveRecord对象。这可能意味着对于大型或频繁运行的查询而言,性能更好。但是,任何模型方法覆盖都将不可用。例如

class Customer < ApplicationRecord
  def name
    "I am #{first_name}"
  end
end
irb> Customer.select(:first_name).map &:name
=> ["I am David", "I am Jeremy", "I am Jose"]

irb> Customer.pluck(:first_name)
=> ["David", "Jeremy", "Jose"]

您不仅限于查询单个表的字段,还可以查询多个表。

irb> Order.joins(:customer, :books).pluck("orders.created_at, customers.email, books.title")

此外,与select和其他Relation作用域不同,pluck会触发立即查询,因此无法与任何进一步的作用域链接,尽管它可以与之前构建的作用域一起使用

irb> Customer.pluck(:first_name).limit(1)
NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>

irb> Customer.limit(1).pluck(:first_name)
=> ["David"]

您还应该知道,如果关系对象包含包含值,则使用pluck将触发急切加载,即使急切加载对于查询来说不是必需的。例如

irb> assoc = Customer.includes(:reviews)
irb> assoc.pluck(:id)
SELECT "customers"."id" FROM "customers" LEFT OUTER JOIN "reviews" ON "reviews"."id" = "customers"."review_id"

避免这种情况的一种方法是unscope包含内容

irb> assoc.unscope(:includes).pluck(:id)

19.3 pick

pick 可用于从当前关系中指定列获取值。它接受一个列名列表作为参数,并返回指定列值的对应数据类型的第一行。pickrelation.limit(1).pluck(*column_names).first 的简写,当您已经有一个限制为一行的关系时,它非常有用。

pick 使替换以下代码成为可能:

Customer.where(id: 1).pluck(:id).first

使用

Customer.where(id: 1).pick(:id)

19.4 ids

ids 可用于使用表的主键获取关系的所有 ID。

irb> Customer.ids
SELECT id FROM customers
class Customer < ApplicationRecord
  self.primary_key = "customer_id"
end
irb> Customer.ids
SELECT customer_id FROM customers

20 对象的存在

如果您只想检查对象是否存在,可以使用一个名为 exists? 的方法。该方法将使用与 find 相同的查询查询数据库,但它不会返回对象或对象集合,而是返回 truefalse

Customer.exists?(1)

exists? 方法也可以接受多个值,但它只会在其中一个记录存在时返回 true

Customer.exists?(id: [1, 2, 3])
# or
Customer.exists?(first_name: ["Jane", "Sergei"])

您甚至可以在没有参数的情况下在模型或关系上使用 exists?

Customer.where(first_name: "Ryan").exists?

如果存在至少一个 first_name 为 'Ryan' 的客户,则以上将返回 true,否则返回 false

Customer.exists?

如果 customers 表为空,则以上将返回 false,否则返回 true

您也可以使用 any?many? 来检查模型或关系的存在性。many? 将使用 SQL count 来确定项目是否存在。

# via a model
Order.any?
# SELECT 1 FROM orders LIMIT 1
Order.many?
# SELECT COUNT(*) FROM (SELECT 1 FROM orders LIMIT 2)

# via a named scope
Order.shipped.any?
# SELECT 1 FROM orders WHERE orders.status = 0 LIMIT 1
Order.shipped.many?
# SELECT COUNT(*) FROM (SELECT 1 FROM orders WHERE orders.status = 0 LIMIT 2)

# via a relation
Book.where(out_of_print: true).any?
Book.where(out_of_print: true).many?

# via an association
Customer.first.orders.any?
Customer.first.orders.many?

21 计算

本节使用 count 作为该前言中的示例方法,但所述选项适用于所有子节。

所有计算方法都直接作用于模型

irb> Customer.count
SELECT COUNT(*) FROM customers

或关系

irb> Customer.where(first_name: 'Ryan').count
SELECT COUNT(*) FROM customers WHERE (first_name = 'Ryan')

您也可以在关系上使用各种查找方法来执行复杂的计算

irb> Customer.includes("orders").where(first_name: 'Ryan', orders: { status: 'shipped' }).count

这将执行

SELECT COUNT(DISTINCT customers.id) FROM customers
  LEFT OUTER JOIN orders ON orders.customer_id = customers.id
  WHERE (customers.first_name = 'Ryan' AND orders.status = 0)

假设 Order 有 enum status: [ :shipped, :being_packed, :cancelled ]

21.1 count

如果您想查看模型表中有多少记录,您可以调用 Customer.count,它将返回数字。如果您想更具体地查找数据库中所有具有标题的客户,您可以使用 Customer.count(:title)

有关选项,请参阅父节,计算

21.2 average

如果您想查看表中某个数字的平均值,您可以在与该表相关的类上调用 average 方法。此方法调用将类似于:

Order.average("subtotal")

这将返回一个数字(可能是一个浮点数,例如 3.14159265),表示该字段的平均值。

有关选项,请参阅父节,计算

21.3 minimum

如果您想查找表中字段的最小值,您可以在与该表相关的类上调用 minimum 方法。此方法调用将类似于:

Order.minimum("subtotal")

有关选项,请参阅父节,计算

21.4 maximum

如果您想查找表中字段的最大值,您可以在与该表相关的类上调用 maximum 方法。此方法调用将类似于:

Order.maximum("subtotal")

有关选项,请参阅父节,计算

21.5 sum

如果您想查找表中所有记录的字段总和,您可以在与该表相关的类上调用 sum 方法。此方法调用将类似于:

Order.sum("subtotal")

有关选项,请参阅父节,计算

22 运行 EXPLAIN

您可以在关系上运行 explain。EXPLAIN 输出因数据库而异。

例如,运行

Customer.where(id: 1).joins(:orders).explain

对于 MySQL 和 MariaDB 可能会产生:

EXPLAIN SELECT `customers`.* FROM `customers` INNER JOIN `orders` ON `orders`.`customer_id` = `customers`.`id` WHERE `customers`.`id` = 1
+----+-------------+------------+-------+---------------+
| id | select_type | table      | type  | possible_keys |
+----+-------------+------------+-------+---------------+
|  1 | SIMPLE      | customers  | const | PRIMARY       |
|  1 | SIMPLE      | orders     | ALL   | NULL          |
+----+-------------+------------+-------+---------------+
+---------+---------+-------+------+-------------+
| key     | key_len | ref   | rows | Extra       |
+---------+---------+-------+------+-------------+
| PRIMARY | 4       | const |    1 |             |
| NULL    | NULL    | NULL  |    1 | Using where |
+---------+---------+-------+------+-------------+

2 rows in set (0.00 sec)

Active Record 执行类似于相应数据库 shell 的格式化输出。因此,使用 PostgreSQL 适配器运行相同的查询将改为产生

EXPLAIN SELECT "customers".* FROM "customers" INNER JOIN "orders" ON "orders"."customer_id" = "customers"."id" WHERE "customers"."id" = $1 [["id", 1]]
                                  QUERY PLAN
------------------------------------------------------------------------------
 Nested Loop  (cost=4.33..20.85 rows=4 width=164)
    ->  Index Scan using customers_pkey on customers  (cost=0.15..8.17 rows=1 width=164)
          Index Cond: (id = '1'::bigint)
    ->  Bitmap Heap Scan on orders  (cost=4.18..12.64 rows=4 width=8)
          Recheck Cond: (customer_id = '1'::bigint)
          ->  Bitmap Index Scan on index_orders_on_customer_id  (cost=0.00..4.18 rows=4 width=0)
                Index Cond: (customer_id = '1'::bigint)
(7 rows)

渴望加载可能在幕后触发多个查询,并且某些查询可能需要之前查询的结果。因此,explain 实际上执行查询,然后请求查询计划。例如,运行

Customer.where(id: 1).includes(:orders).explain

对于 MySQL 和 MariaDB 可能会产生:

EXPLAIN SELECT `customers`.* FROM `customers`  WHERE `customers`.`id` = 1
+----+-------------+-----------+-------+---------------+
| id | select_type | table     | type  | possible_keys |
+----+-------------+-----------+-------+---------------+
|  1 | SIMPLE      | customers | const | PRIMARY       |
+----+-------------+-----------+-------+---------------+
+---------+---------+-------+------+-------+
| key     | key_len | ref   | rows | Extra |
+---------+---------+-------+------+-------+
| PRIMARY | 4       | const |    1 |       |
+---------+---------+-------+------+-------+

1 row in set (0.00 sec)

EXPLAIN SELECT `orders`.* FROM `orders`  WHERE `orders`.`customer_id` IN (1)
+----+-------------+--------+------+---------------+
| id | select_type | table  | type | possible_keys |
+----+-------------+--------+------+---------------+
|  1 | SIMPLE      | orders | ALL  | NULL          |
+----+-------------+--------+------+---------------+
+------+---------+------+------+-------------+
| key  | key_len | ref  | rows | Extra       |
+------+---------+------+------+-------------+
| NULL | NULL    | NULL |    1 | Using where |
+------+---------+------+------+-------------+


1 row in set (0.00 sec)

并可能为 PostgreSQL 产生:

  Customer Load (0.3ms)  SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1  [["id", 1]]
  Order Load (0.3ms)  SELECT "orders".* FROM "orders" WHERE "orders"."customer_id" = $1  [["customer_id", 1]]
=> EXPLAIN SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 [["id", 1]]
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Scan using customers_pkey on customers  (cost=0.15..8.17 rows=1 width=164)
   Index Cond: (id = '1'::bigint)
(2 rows)

22.1 Explain 选项

对于支持这些选项的数据库和适配器(目前为 PostgreSQL、MySQL 和 MariaDB),可以传递选项以提供更深入的分析。

使用 PostgreSQL,以下

Customer.where(id: 1).joins(:orders).explain(:analyze, :verbose)

产生

EXPLAIN (ANALYZE, VERBOSE) SELECT "shop_accounts".* FROM "shop_accounts" INNER JOIN "customers" ON "customers"."id" = "shop_accounts"."customer_id" WHERE "shop_accounts"."id" = $1 [["id", 1]]
                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.30..16.37 rows=1 width=24) (actual time=0.003..0.004 rows=0 loops=1)
   Output: shop_accounts.id, shop_accounts.customer_id, shop_accounts.customer_carrier_id
   Inner Unique: true
   ->  Index Scan using shop_accounts_pkey on public.shop_accounts  (cost=0.15..8.17 rows=1 width=24) (actual time=0.003..0.003 rows=0 loops=1)
         Output: shop_accounts.id, shop_accounts.customer_id, shop_accounts.customer_carrier_id
         Index Cond: (shop_accounts.id = '1'::bigint)
   ->  Index Only Scan using customers_pkey on public.customers  (cost=0.15..8.17 rows=1 width=8) (never executed)
         Output: customers.id
         Index Cond: (customers.id = shop_accounts.customer_id)
         Heap Fetches: 0
 Planning Time: 0.063 ms
 Execution Time: 0.011 ms
(12 rows)

使用 MySQL 或 MariaDB,以下

Customer.where(id: 1).joins(:orders).explain(:analyze)

产生

ANALYZE SELECT `shop_accounts`.* FROM `shop_accounts` INNER JOIN `customers` ON `customers`.`id` = `shop_accounts`.`customer_id` WHERE `shop_accounts`.`id` = 1
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | r_rows | filtered | r_filtered | Extra                          |
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
|  1 | SIMPLE      | NULL  | NULL | NULL          | NULL | NULL    | NULL | NULL | NULL   | NULL     | NULL       | no matching row in const table |
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
1 row in set (0.00 sec)

EXPLAIN 和 ANALYZE 选项在不同的 MySQL 和 MariaDB 版本中有所不同。(MySQL 5.7, MySQL 8.0, MariaDB)

22.2 解释 EXPLAIN

解释 EXPLAIN 输出的含义超出了本指南的范围。以下提示可能会有所帮助



返回顶部