edge
更多信息请访问 rubyonrails.org: 更多 Ruby on Rails

Active Record查询接口

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

阅读本指南后,您将了解到:

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

所有书店模型的图表

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方法,您可以检索与任何提供的选项匹配的指定主键对应的对象。例如:

# 查找主键(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方法并传入一个主键数组。返回的将是一个包含所有匹配的记录的数组,供应用主键。例如: ```irb

查找主键为1和10的客户。

irb> customers = Customer.find([1, 10]) # 或者 Customer.find(1, 10) => [#, #] ```

上述代码的SQL等效语句为:

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

警告:如果没有找到与所有提供的主键匹配的记录,find方法将引发ActiveRecord::RecordNotFound异常。

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

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

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,不会引发异常。

如果您的默认作用域包含一个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将返回按指定属性进行排序的最后一条记录。

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.2 批量检索多个对象

我们经常需要迭代处理大量的记录,比如向一大批客户发送通讯,或者导出数据。

这种方法可能看起来很简单:

# 如果表很大,这可能会消耗太多的内存。
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。例如,如果您想要恢复一个中断的批处理过程,只发送给从2000开始的客户:

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

:finish

start选项类似,finish选项允许您在最高ID不是您所需的ID时配置序列的最后一个ID。例如,如果您想要使用基于startfinish的记录子集运行批处理,只发送给从2000开始到10000的客户:

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

另一个例子是如果您想要多个工作进程处理相同的处理队列。您可以通过为每个工作进程设置适当的startfinish选项,使每个工作进程处理10000条记录。

:error_on_ignore

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

:order

指定主键的排序顺序(可以是:asc:desc)。默认为:ascruby 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 个客户的数组,最后一个块包含任何剩余的客户:

# 每次给 add_customers 传递一个包含 1000 个客户的数组。
Customer.find_in_batches do |customers|
  export.add_customers(customers)
end

find_in_batches 可以用于模型类,如上所示,也可以用于关系:

# 每次给 add_customers 传递一个包含 1000 个最近活跃客户的数组。
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'") 一样。这将找到 title 字段值为 '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 通配符(即 %_不会被转义。如果在参数中使用未经过处理的值,可能会导致意外行为。例如: ruby Book.order(:created_at).order(:title)

This will generate SQL like this:

SELECT * FROM books ORDER BY created_at ASC, title ASC

You can also use the reorder method to replace any existing order with a new one:

Book.order(:created_at).reorder(:title)

This will generate SQL like this:

SELECT * FROM books ORDER BY title ASC

3.3 Random Ordering

To retrieve records in a random order, you can use the order method with the RANDOM() function:

Book.order("RANDOM()")

This will generate SQL like this:

SELECT * FROM books ORDER BY RANDOM()

3.4 Reverse Ordering

To retrieve records in reverse order, you can use the reverse_order method:

Book.order(:created_at).reverse_order

This will generate SQL like this:

SELECT * FROM books ORDER BY created_at DESC

3.5 Custom Ordering

If you need to order records using a custom SQL expression, you can use the order method with a string argument:

Book.order("CASE WHEN out_of_print = 'true' THEN 0 ELSE 1 END")

This will generate SQL like this:

SELECT * FROM books ORDER BY CASE WHEN out_of_print = 'true' THEN 0 ELSE 1 END

3.6 Nulls Ordering

By default, NULL values are ordered last in ascending order and first in descending order. If you want to change this behavior, you can use the nulls_first or nulls_last methods:

Book.order(:title).nulls_first

This will generate SQL like this:

SELECT * FROM books ORDER BY title ASC NULLS FIRST
Book.order(:title).nulls_last

This will generate SQL like this:

SELECT * FROM books ORDER BY title ASC NULLS LAST

3.7 Reversing Order

To reverse the order of a relation, you can use the reverse method:

Book.order(:created_at).reverse

This will generate SQL like this:

SELECT * FROM books ORDER BY created_at DESC

3.8 Limit and Offset

To limit the number of records returned from the database, you can use the limit method:

Book.limit(10)

This will generate SQL like this:

SELECT * FROM books LIMIT 10

To skip a certain number of records and return the rest, you can use the offset method:

Book.offset(5)

This will generate SQL like this:

SELECT * FROM books OFFSET 5

You can also chain limit and offset together:

Book.limit(10).offset(5)

This will generate SQL like this:

SELECT * FROM books LIMIT 10 OFFSET 5

3.9 Selecting Specific Fields

To select specific fields from the database, you can use the select method:

Book.select(:title, :author)

This will generate SQL like this:

SELECT title, author FROM books

You can also use the select method with a string argument to select fields using custom SQL expressions:

Book.select("title, author, COUNT(*) as count")

This will generate SQL like this:

SELECT title, author, COUNT(*) as count FROM books

3.10 Distinct Records

To retrieve distinct records from the database, you can use the distinct method:

Book.select(:author).distinct

This will generate SQL like this:

SELECT DISTINCT author FROM books

3.11 Grouping Records

To group records together based on a specific field, you can use the group method:

Book.group(:author)

This will generate SQL like this:

SELECT * FROM books GROUP BY author

You can also use the group method with multiple fields:

Book.group(:author, :category)

This will generate SQL like this:

SELECT * FROM books GROUP BY author, category

3.12 Having Conditions

To filter grouped records based on a condition, you can use the having method:

Book.group(:author).having("COUNT(*) > 5")

This will generate SQL like this:

SELECT * FROM books GROUP BY author HAVING COUNT(*) > 5

You can also use the having method with a hash condition:

Book.group(:author).having(count: 5..10)

This will generate SQL like this:

SELECT * FROM books GROUP BY author HAVING count BETWEEN 5 AND 10

3.13 Joins

To join records from multiple tables, you can use the joins method:

Book.joins(:author)

This will generate SQL like this:

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

You can also specify the type of join to use:

Book.joins("LEFT JOIN authors ON authors.id = books.author_id")

This will generate SQL like this:

SELECT * FROM books LEFT JOIN authors ON authors.id = books.author_id

3.14 Eager Loading Associations

To eager load associations and avoid the N+1 query problem, you can use the includes method:

Book.includes(:author)

This will generate SQL like this:

SELECT * FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id

You can also specify multiple associations to eager load:

Book.includes(:author, :category)

This will generate SQL like this:

SELECT * FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id LEFT OUTER JOIN categories ON categories.id = books.category_id

3.15 Preloading Associations

To preload associations and avoid the N+1 query problem, you can use the preload method:

Book.preload(:author)

This will generate SQL like this:

SELECT * FROM books
SELECT * FROM authors WHERE authors.id IN (1, 2, 3, ...)

You can also specify multiple associations to preload:

Book.preload(:author, :category)

This will generate SQL like this:

SELECT * FROM books
SELECT * FROM authors WHERE authors.id IN (1, 2, 3, ...)
SELECT * FROM categories WHERE categories.id IN (1, 2, 3, ...)

3.16 References

When using the includes or preload methods with associations, you may need to use the references method to specify the table name:

Book.includes(:author).references(:authors)

This will generate SQL like this:

SELECT * FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id

3.17 Locking Records

To lock records and prevent them from being modified by other transactions, you can use the lock method:

Book.lock

This will generate SQL like this:

SELECT * FROM books FOR UPDATE

You can also specify the type of lock to use:

Book.lock("LOCK IN SHARE MODE")

This will generate SQL like this:

SELECT * FROM books LOCK IN SHARE MODE

3.18 Calculations

To perform calculations on records, you can use the count, sum, average, minimum, and maximum methods:

Book.count

This will generate SQL like this:

SELECT COUNT(*) FROM books
Book.sum(:price)

This will generate SQL like this:

SELECT SUM(price) FROM books
Book.average(:rating)

This will generate SQL like this:

SELECT AVG(rating) FROM books
Book.minimum(:published_at)

This will generate SQL like this:

SELECT MIN(published_at) FROM books
Book.maximum(:published_at)

This will generate SQL like this:

SELECT MAX(published_at) FROM books

3.19 Pluck

To retrieve a single column from the database, you can use the pluck method:

Book.pluck(:title)

This will generate SQL like this:

SELECT title FROM books

You can also use the pluck method with multiple columns:

Book.pluck(:title, :author)

This will generate SQL like this:

SELECT title, author FROM books

3.20 Batches

To process records in batches, you can use the find_each or find_in_batches methods:

Book.find_each do |book|
  # Process each book
end

This will retrieve records in batches of 1000 (by default) and yield each record to the block.

Book.find_in_batches(batch_size: 500) do |books|
  # Process each batch of books
end

This will retrieve records in batches of 500 and yield each batch to the block.

3.21 Query Methods

Active Record provides a set of query methods that can be used to build complex queries:

  • where
  • not
  • or
  • and
  • order
  • reorder
  • limit
  • offset
  • select
  • distinct
  • group
  • having
  • joins
  • includes
  • preload
  • references
  • lock
  • count
  • sum
  • average
  • minimum
  • maximum
  • pluck
  • find_each
  • find_in_batches

These methods can be chained together to build powerful and flexible queries. irb irb> Book.order("title ASC").order("created_at DESC") SELECT * FROM books ORDER BY title ASC, created_at DESC

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

4 选择特定字段

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

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

例如,要仅选择isbnout_of_print列:

Book.select(:isbn, :out_of_print)
# 或者
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

您还可以删除唯一性约束:

# 返回唯一的last_names
query = Customer.select(:last_name).distinct

# 返回所有的last_names,即使有重复
query.distinct(false)

5 限制和偏移

要在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

6 分组

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

例如,如果要查找订单创建日期的集合:

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

这将为数据库中存在订单的每个日期提供一个单独的Order对象。

执行的SQL将类似于:

SELECT created_at
FROM orders
GROUP BY created_at

6.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

6.2 HAVING条件

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

例如:

Order.select("created_at, 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
# 返回第一个订单对象的总价格

7 覆盖条件

7.1 unscope

您可以使用unscope方法指定要删除的某些条件。例如: ruby Book.where('id > 100').limit(20).order('id desc').unscope(:order)

执行的 SQL 语句:

SELECT * FROM books WHERE id > 100 LIMIT 20

-- 没有 `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

7.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

-- 没有 `only` 的原始查询
SELECT * FROM books WHERE id > 10 ORDER BY id DESC LIMIT 20

7.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

7.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

7.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 方法不接受任何参数。

7.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

7.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

8 空关系

none 方法返回一个可链式操作的空关系,没有记录。返回的关系链上的任何后续条件都将继续生成空关系。这在需要对可能返回零结果的方法或作用域进行链式响应的场景中非常有用。

Book.none # 返回一个空的关系,并且不执行任何查询。
# 下面的 highlighted_reviews 方法预期始终返回一个关系。
Book.first.highlighted_reviews.average(:rating)
# => 返回书籍的平均评分

class Book
  # 如果评论数大于 5,则返回评论,否则将其视为未评论的书籍
  def highlighted_reviews
    if reviews.count > 5
      reviews
    else
      Review.none # 还未达到最低阈值
    end
  end
end

9 只读对象

Active Record 在关系上提供了 readonly 方法,用于明确禁止修改返回的任何对象。任何尝试修改只读记录的操作都将失败,并引发 ActiveRecord::ReadOnlyRecord 异常。 ruby customer = Customer.readonly.first customer.visits += 1 customer.save

由于customer被显式设置为只读对象,上述代码在调用customer.save时,如果更新了visits的值,将会引发ActiveRecord::ReadOnlyRecord异常。

10 锁定记录以进行更新

锁定有助于防止在数据库中更新记录时出现竞争条件,并确保原子更新。

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

  • 乐观锁定
  • 悲观锁定

10.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 # 引发ActiveRecord::StaleObjectError异常

然后,您需要通过捕获异常来处理冲突,并根据需要回滚、合并或应用解决冲突所需的业务逻辑。

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

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

class Customer < ApplicationRecord
  self.locking_column = :lock_customer_column
end

10.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
  # 此块在事务中调用,
  # book已经被锁定。
  book.increment!(:views)
end

11 连接表

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

11.1 joins

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

11.1.1 使用字符串SQL片段

您可以直接提供指定JOIN子句的原始SQL给joins

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

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

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

以下所有方法都将使用INNER JOIN生成预期的连接查询:

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

这将产生:

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

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

11.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

或者,用英文来说:“返回所有至少有一条评论的带有作者的书籍”。请注意,有多个评论的书籍会出现多次。

11.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

或者,用英文来说:“返回所有有顾客评论的书籍”。

11.1.3.2 加入嵌套关联(多层)
Author.joins(books: [{ reviews: { customer: :orders } }, :supplier])

这将产生:

SELECT * 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

或者,用英文来说:“返回所有有评论的书籍的作者,并且这些书籍被顾客订购过,以及这些书籍的供应商”。

11.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

现在我们可以使用merge来合并created_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表达式。

11.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

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

11.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

这意味着“返回没有发表任何评论的所有顾客”。

12 预加载关联

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

12.1 N + 1 查询问题

考虑以下代码,它查找10本书并打印它们作者的姓氏:

books = Book.limit(10)

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

这段代码乍一看没问题。但问题在于执行的查询总数。上述代码总共执行了 1 次(查找10本书)+ 10 次(每本书加载作者)= 11 次查询。

12.1.1 解决 N + 1 查询问题

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

方法有:

12.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.book_id IN (1,2,3,4,5,6,7,8,9,10)

12.2.1 预加载多个关联

Active Record允许您使用单个Model.find调用使用数组、哈希或嵌套的数组/哈希来预加载任意数量的关联。

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

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

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

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

12.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的参数是哈希时,才能像这样使用where。对于SQL片段,您需要使用references来强制连接的表:

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

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

注意:如果一个关联作为连接的一部分被急加载,那么自定义选择子句中的任何字段都不会出现在加载的模型上。这是因为它们可能出现在父记录上,也可能出现在子记录上,所以是模棱两可的。

12.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.book_id IN (1,2,3,4,5,6,7,8,9,10)

注意:preload方法与includes方法一样,使用数组、哈希或嵌套的数组/哈希以与Model.find调用一样的方式加载任意数量的关联。然而,与includes方法不同的是,无法为预加载的关联指定条件。

12.4 eager_load

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

重新访问N + 1查询问题,我们可以将Book.limit(10)重写为作者:

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

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

上面的代码将执行2个查询,而原始情况下执行了11个查询:

SELECT DISTINCT books.id FROM books LEFT OUTER JOIN authors ON authors.book_id = books.id LIMIT 10
SELECT books.id AS t0_r0, books.last_name AS t0_r1, ...
  FROM books LEFT OUTER JOIN authors ON authors.book_id = books.id
  WHERE books.id IN (1,2,3,4,5,6,7,8,9,10)

注意:eager_load方法与includes方法一样,使用数组、哈希或嵌套的数组/哈希以与Model.find调用一样的方式加载任意数量的关联。而且,与includes方法一样,您可以为急加载的关联指定条件。

12.5 strict_loading

急加载可以防止N + 1查询,但您可能仍然会惰性加载一些关联。为了确保没有关联被惰性加载,您可以启用strict_loading

通过在关系上启用严格加载模式,如果记录尝试惰性加载关联,则会引发ActiveRecord::StrictLoadingViolationError

user = User.strict_loading.first
user.comments.to_a # 引发ActiveRecord::StrictLoadingViolationError

13 作用域

作用域允许您指定常用的查询,这些查询可以在关联对象或模型上作为方法调用引用。通过这些作用域,您可以使用之前介绍的所有方法,例如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> # 所有已绝版的书籍

或者在由Book对象组成的关联上调用它:

irb> author = Author.first
irb> author.books.out_of_print
=> #<ActiveRecord::Relation> # `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

13.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)

13.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

然而,有一个重要的注意事项:作用域始终会返回一个ActiveRecord::Relation对象,即使条件计算结果为false,而类方法会返回nil。如果任何条件返回false,这可能会导致在链接类方法时出现NoMethodError

13.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
    # 应该返回一个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>

13.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_scopescopewhere 条件中都被合并。

13.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

14 动态查找器

对于您在表中定义的每个字段(也称为属性),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)

15 枚举

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

声明枚举将:

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

对于枚举的所有可能值。

例如,给定此 enum 声明:

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

这些 作用域 将自动生成,并可用于查找具有或不具有 status 的特定值的所有对象:

irb> Order.shipped
=> #<ActiveRecord::Relation> # 所有状态为 :shipped 的订单
irb> Order.not_shipped
=> #<ActiveRecord::Relation> # 所有状态不为 :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

有关枚举的完整文档可以在此处找到。

16 理解方法链

Active Record 模式实现了方法链,允许我们以简单直接的方式在多个 Active Record 方法之间进行链式调用。

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

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

16.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')

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

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

以上代码应该生成以下SQL语句:

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语句)。

17 查找或创建新对象

通常情况下,您需要查找一条记录,如果不存在则创建。您可以使用find_or_create_byfind_or_create_by!方法来实现。

17.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

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

17.2 find_or_create_by!

您还可以使用find_or_create_by!方法,如果新记录无效,则引发异常。本指南不涵盖验证,但假设您暂时添加了以下代码到您的Customer模型中:

validates :orders_count, presence: true

如果尝试创建一个没有传递orders_count的新Customer,记录将无效并引发异常:

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

17.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

18 通过SQL查找

如果您想要在表中使用自己的SQL查找记录,可以使用find_by_sql方法。find_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提供了一种简单的方法,可以自定义调用数据库并检索实例化对象。

18.1 select_all

find_by_sql有一个紧密相关的方法叫做connection.select_allselect_all将使用自定义的SQL从数据库中检索对象,但不会实例化它们。该方法将返回一个ActiveRecord::Result类的实例,调用该实例的to_a方法将返回一个哈希数组,其中每个哈希表示一条记录。

irb> Customer.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"}]

18.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 }
# 或者
Customer.select(:id).map(&:id)
# 或者
Customer.select(:id, :first_name).map { |c| [c.id, c.first_name] }

使用以下代码替代:

Customer.pluck(:id)
# 或者
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"]

注意:您还应该知道,如果关系对象包含include值,使用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"

避免这种情况的一种方法是对includes使用unscope

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

18.3 pick

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

pick可以替代以下代码:

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

使用以下代码替代:

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

18.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

19 对象的存在性

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

Customer.exists?(1)

exists?方法还接受多个值,但是它会在任何一个记录存在时返回true

Customer.exists?(id: [1, 2, 3])
# 或者
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来确定项目是否存在。 ```ruby

通过模型

Order.any?

SELECT 1 FROM orders LIMIT 1

Order.many?

SELECT COUNT(*) FROM (SELECT 1 FROM orders LIMIT 2)

通过命名作用域

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)

通过关联

Book.where(out_of_print: true).any? Book.where(out_of_print: true).many?

通过关联

Customer.first.orders.any? Customer.first.orders.many? ```

20 计算

本节以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 ]

20.1 count

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

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

20.2 average

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

Order.average("subtotal")

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

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

20.3 minimum

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

Order.minimum("subtotal")

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

20.4 maximum

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

Order.maximum("subtotal")

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

20.5 sum

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

Order.sum("subtotal")

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

21 运行EXPLAIN

您可以在关联上运行explain。每个数据库的EXPLAIN输出都不同。

例如,运行

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

可能会产生以下结果

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)

在MySQL和MariaDB下。

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实际上会执行查询,然后请求查询计划。例如, ruby 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)

21.1 Explain选项

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

使用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.7MySQL 8.0MariaDB

21.2 解释EXPLAIN

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

反馈

欢迎您帮助改进本指南的质量。

如果您发现任何拼写错误或事实错误,请贡献您的意见。 要开始,请阅读我们的 文档贡献 部分。

您还可能会发现不完整的内容或过时的内容。 请为主要内容添加任何缺失的文档。请先检查 Edge 指南,以验证问题是否已经修复或尚未修复。 请参阅 Ruby on Rails 指南准则 以了解样式和规范。

如果您发现需要修复但无法自行修复的问题,请 提交问题

最后但同样重要的是,欢迎您在 官方 Ruby on Rails 论坛 上讨论有关 Ruby on Rails 文档的任何问题。