>> user.destroy
Article destroyed
=> #
介入Active Rccord 对象的生命周期
对象的生命周期:
在rails应用正常运作期间,对象可以被创建、更新或删除。Active Record为对象的生命周期提供了钩子,使我们可以控制应用及其数据。
回调:
我们可以在对象状态更改之前或之后触发逻辑。
例:
class User < ApplicationRecord
before_validation :normalize_name, on: :create
# :on 选项的值也可以是数组
after_validation :set_location, on: [ :create, :update ]
private
def normalize_name
self.name = name.downcase.titleize
end
def set_location
self.location = LocationService.query(self)
end
end
通过:on :回调注册仅被某些生命周期时间触发。
通常应该把回调定义为私有方法。
可用的回调及执行顺序
创建对象:
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback
更新对象:
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit/after_rollback
删除对象:
before_destory
around_destory
after_destory
after_commit/after_rlooback
无论按什么顺序注册回调,在创建和更新对象时,after_save回调总是在更明确的after_create和after_update回调之后被调用。
after_initialize和after_find回调
对象呗实例化时,不管是直接new还是从数据库中加载,都会调用after_initialize回调。避免覆盖Active Record的initilalize方法。
如果同时定义了 after_initialize 和 after_find回调,会先调用 after_find 回调。
after_initialize 和 after_find 回调没有对应的 before_* 回调,这两个回调的注册方式和其他 Active Record 回调一样。
after_touch回调
当我们在 Active Record 对象上调用 touch 方法时,会调用 after_touch 回调。
调用回调
下面的方法会触发回调:
create
create!
decrement!
destory
destory!
destory_all
increment!
save
save!
save(validate:false)
toggle!
update_attribute
update
update!
valid?
下面这些会触发after_find回调:
all
first
find
find_by
find_by_*
find_by_*!
find_by_sql
last
每次初始化类的新对象时都会触发after_initilalize回调。
跳过回调
使用下面这些方法可以跳过回调:
decrement
decrement_count
delete
delete_all
increment
increment_count
toggle
touch
update_column
update_columns
update_all
update_counters
慎重使用,跳过回调,可能导致无效数据。
停止执行
回调在模型中注册后,将被加入队列等待执行。这个队列包含了所有模型的验证、已注册的回调和将要执行的数据库操作。
整个回调链包装在一个事物中。只要有回调抛出异常,回调链随即停止,并且发出ROLLBACK消息。
你也可以主动停止回调链:
throw:abord
关联回调
通过模型之间的关联,执行关联模型对象的相关回调操作。
假设有一个用户在博客中发表了多篇文章,现在我们要删除这个用户,那么这个用户的所有文章也应该删除,为此我们通过 Article模型和 User 模型的关联来给 User模型添加一个 after_destroy回调:
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class Article < ApplicationRecord
after_destroy :log_destroy_action
def log_destroy_action
puts 'Article destroyed'
end
end
?
>> user = User.first
=> #
>> user.articles.create!
=> #
>> user.destroy
Article destroyed
=> #
条件回调
和条件验证一样,可以在满足条件的情况下再调用回调方法。
class Comment < ApplicationRecord
after_create :send_email_to_author, if: :author_wants_emails?,
unless: Proc.new { |comment| comment.article.ignore_comments? }
end
回调类
把回调方法单独放到一个类中。
注意:一般吧回调声明为类方法,这样既有不用实例化对象。
class PictureFileCallbacks
def after_destroy(picture_file)
if File.exist?(picture_file.filepath)
File.delete(picture_file.filepath)
end
end
end
?
class PictureFile < ApplicationRecord
after_destroy PictureFileCallbacks.new
end
?
?
class PictureFileCallbacks
def self.after_destroy(picture_file)
if File.exist?(picture_file.filepath)
File.delete(picture_file.filepath)
end
end
end
?
class PictureFile < ApplicationRecord
after_destroy PictureFileCallbacks
end
可以根据需要在回调类中声明任意多个回调。
事务回调
after_commit和after_rollback这两个回调会在数据库事务完成时触发。
PictureFile模型中的记录删除后,还要删除相应的文件。如果 after_destroy 回调执行后应用引发异常,事务就会回滚,文件会被删除,模型会保持不一致的状态。例如,假设在下面的代码中,picture_file_2对象是无效的,那么调用 save!`方法会引发错误:
PictureFile.transaction do
picture_file_1.destroy
picture_file_2.save!
end
?
?
class PictureFile < ApplicationRecord
after_commit :delete_picture_file_from_disk, on: :destroy
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
执行回调,保持事务同步。
由于只在执行创建、更新或删除动作时触发 after_commit回调是很常见的,这些操作都拥有别名:
after_create_commit
after_update_commit
after_destroy_commit
class PictureFile < ApplicationRecord
after_destroy_commit :delete_picture_file_from_disk
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
在事务中创建、更新或删除模型时会调用 after_commit
和 after_rollback
回调。然而,如果其中有一个回调引发异常,异常会向上冒泡,后续 after_commit
和 after_rollback
回调不再执行。因此,如果回调代码可能引发异常,就需要在回调中救援并进行适当处理,以便让其他回调继续运行。
Active Record查询接口
使用Active Record从数据库中检索数据的不同方法。
在rails中无需写sql语句,可直接使用封装好的方法。
find
查询单个对象
client=Client.find(10)
等价于
SELECT * FROM clients WHERE(client.id=10) LIMIT 1
?
查询多个对象
client=Client.find([1,10])
等价于
SELECT * FROM clients WHERE (client.id IN(1,10))
如果所提供的主键都没有匹配记录,那么find方法会抛出ActiveRecord::RecordNotFound异常。
take
take方法检索一条数据,随机的没有顺序。
client=Client.take
等价于
SELECT * FROM clients LIMIT 1
如果没有找到记录,返回nil,不抛出异常
?
client=Client.take(2)
等价于
SELECT * FROM clients LIMIT 2
take!方法的行为和take方法类似,区别在于,没找到会抛异常。
first
默认查找按主键排序的第一条记录
client=Client.first
等价于
SELECT * FROM clients ORDER BY client.id ASC LIMIT 1
没找到,返回nil,不会抛异常
?
client=Client.first(3)
等价于
SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3
?
也可以按指定的属性进行排列
client=Client.order(:first_name).first
等价于
SELECE * FROM clients ORDER BY clients.first_name ASC LIMIT 1
first!方法和first类似,区别在于,没找到会抛异常。
last
默认查找主键排序的最后一条记录
client=Client.last
等价于
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
没找到返回nil,不会抛异常
?
client=Client.last(3)
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3
?
client=Client.order(:first_name).last
SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1
find_by
查找匹配指定条件的第一条记录
Client.find_by first_name:'LIfo'
Client.where(first_name:'Lifo').take
SELECT * FROM clients WHERE(clients.first_name='Lifo')LIMIT 1
?
批量检索多个对象
一次取出所有的数据的话,为每条记录创建模型对象,并把整个模型对象都存入内存中。
不要一次性保存在内存中,两种方法来处理。
find_each
每次检索一批记录,然后逐一吧每条记录作为模型传入块。
User.find_each do |user|
NewMailer.weekly(user).deliver_now
end
:batch_size
指定批量处理时,一次检索多少条记录。
User.find_each(batch_size:5000) do |user|
NewMailer.weekly(user).deliver_now
end
:start :finish
User.find_each(start:2000,finish:10000) do |user|
NewMailer.weekly(user).deliver_now
end
find_in_batches
把一批记录作为模型数组传入块。
Invoice.find_in_batches do |invoices|
export.add_invoices(invoices)
end
条件查询
Client.where("orders_count=?",params[:orders])
?
Client.where("orders_count=? AND locked=?",params[:orders],false)
?
Client.where("created_at >= :start_date AND creates_at <= :end_date"),
{start_date:params[:start_date],end_date:params[:end_date]})
散列条件
在散列条件中,只能进行相等性、范围和子集检查。
相等性条件
Client.where(locked:true)
Client.where('locked'=>true)
SELECT * FROM clients WHERE (clients.locked=1)
?
Article.where(author:author)
Author.joins(:articles).where(articles:{author:author})
?
范围条件
Client.where(create_at:(Time.now.midnight-1.day)..Time.now.midnight)
SELECT * FROM clients WHERE(clients,created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
?
子集条件
用IN表达式来查找记录,在散列条件中使用数组
Client.where(order_count:[1,3,5])
SELECE * FROM clients WHERE (clients.order_count IN (1,3,5))
NOT条件
Client.where.not(locked:true)
SELECT * FROM clients WHERE (clients.locked!=1)
排序
Client.order("order_count ASC").order("created_at DESC")
SELECT * FROM clients ORDER BY orders_count ASC,created_at DESC
选择特定字段
Client.select("viewable_by,locked")
Select viewable_by,locked FROM clients
?
去重
Client.select(:name).distinct
SELECT DISTINCT name FROM clients
限量和偏移量
Client.limit(5)
SELECT * FROM clients LIMIT 5
?
Client.limit(5).offset(30)
SELECT * FROM clients LIMIT 5 OFFSET 30
分组
Order.select("date(create_at) as order_date,sum(price) as total_price").group("date(create_at)")
SELECT date(create_at) as order_date,sum(price) as total_price
FROM orders
GROUP BY date(create_at)
?
分组项目总数:
Order.group(:status).count
SELECT COUNT(*) AS count_all,status AS status
FROM "order"
GROUP BY status
having
指定分组的约束条件
Order.select("date(created_at) as ordered_date,sum(price) as total_price").group("date(created_at)").having("sum(price)>?",100)
?
SELECT date(create_at) as ordered_date,sum(price) as total_price
FROM orders
GROUP BY date(created_at)
HAVING sum(price)>100
条件覆盖
使用 unscope 方法删除某些条件
Article.where('id > 10').limit(20).order('id asc').unscope(:order)
SELECT * FROM articles WHERE id > 10 LIMIT 20
?
使用 only 方法覆盖某些条件
Article.where('id > 10').limit(20).order('id desc').only(:order, :where)
SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
?
使用 reorder 方法覆盖默认作用域中的排序方式
class Article < ApplicationRecord
has_many :comments, -> { order('posted_at DESC') }
end
Article.find(10).comments.reorder('name')
SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY name
?
使用 reverse_order 方法反转排序条件
Client.where("orders_count > 10").order(:name).reverse_order
SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC
?
使用 rewhere 方法覆盖 where 方法中指定的条件
Article.where(trashed: true).rewhere(trashed: false)
SELECT * FROM articles WHERE `trashed` = 0
空关系
none
方法返回可以在链式调用中使用的、不包含任何记录的空关系。在这个空关系上应用后续条件链,会继续生成空关系。对于可能返回零结果、但又需要在链式调用中使用的方法或作用域,可以使用 none
方法来提供返回值。
Article.none # 返回一个空 Relation 对象,而且不执行查询
?
# 下面的 visible_articles 方法期待返回一个空 Relation 对象
@articles = current_user.visible_articles.where(name: params[:name])
def visible_articles
case role
when 'Country Manager'
Article.where(country: country)
when 'Reviewer'
Article.published
when 'Bad User'
Article.none # => 如果这里返回 [] 或 nil,会导致调用方出错
end
end
只读对象
在关联中使用 Active Record 提供的 readonly
方法,可以显式禁止修改任何返回对象。如果尝试修改只读对象,不但不会成功,还会抛出 ActiveRecord::ReadOnlyRecord
异常。
client = Client.readonly.first
在更新时锁定记录
Active Record 提供了两种锁定机制:
乐观锁定
悲观锁定
乐观锁定
c1 = Client.find(1)
c2 = Client.find(1)
c1.first_name = "Michael"
c1.save
c2.name = "should fail"
c2.save # 抛出 ActiveRecord::StaleObjectError
?
?
悲观锁定
在创建关联时使用 lock 方法,会在选定字段上生成互斥锁。使用 lock 方法的关联通常被包装在事务中,以避免发生死锁。
Item.transaction do
i = Item.lock.first
i.name = 'Jones'
i.save!
end
联结表
Active Record 提供了 joins
和 left_outer_joins
这两个查找方法,用于指明生成的 SQL 语句中的 JOIN
子句。其中,joins
方法用于 INNER JOIN
查询或定制查询,left_outer_joins
用于 LEFT OUTER JOIN
查询。
Joins
Author.joins("INNER JOIN posts ON posts.author_id=authors.id AND posts.published='t'")
?
SELECT authors.*FROM authors INNER JOIN posts ON posts.author_id=authors.id AND posts.published='t'
?
?
class Category < ApplicationRecord
has_many :articles
end
class Article < ApplicationRecord
belongs_to :category
has_many :comments
has_many :tags
end
class Comment < ApplicationRecord
belongs_to :article
has_one :guest
end
class Guest < ApplicationRecord
belongs_to :comment
end
class Tag < ApplicationRecord
belongs_to :article
end
?
单个关联的联结
Category.join(:articles)
?
SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id=categories.id
多个关联的联结
Article.joins(:category,:comments)
?
SELECT articles.* FROM articles
INNER JOIN categories ON articles.category_id=categories.id
INNER JOIN comments ON comments.article_id=articles.id
单层嵌套关联的联结
Article.joins(comments: :guest)
?
SELECT articles.* FROM articles
INNER JOIN comments ON comments.article_id=articles.id
INNER JOIN guests ON guests.comment_id=comments.id
多层嵌套关联的联结
Category.joins(articles:[{comments::guest},:tags])
?
SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id=categories.id
INNER JOIN comments ON comments.article_id=articles.id
INNER JOIN guests ON guests.comment_id=comments.id
INNER JOIN tags ON tags.article_id=articles.id
为联结表指明条件
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where('orders.created_at' => time_range)
?
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where(orders: { created_at: time_range })
?
left_outer_joins
选择一组记录,而不管它们是否具有关联记录,可以使用 left_outer_joins
方法。
Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')
?
SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id
返回所有作者和每位作者的帖子数,而不管这些作者是否发过帖子
及早加载关联
尽可能减少查询的次数,提高效率。
N+1查询问题
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
这段代码总共需要执行 1(查找 10 条客户记录)+ 10(每条客户记录都需要加载地址)= 11 次查询
?
?
通过指明 includes 方法,Active Record 会使用尽可能少的查询来加载所有已指明的关联
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
上面的代码只执行 2 次查询,而不是之前的 11 次查询
作用域
作用域允许我们把常用查询定义为方法,然后通过在关联对象或模型上调用方法来引用这些查询。fotnote:[“作用域”和“作用域方法”在本文中是一个意思。——译者注]在作用域中,我们可以使用之前介绍过的所有方法,如 where
、join
和 includes
方法。所有作用域都会返回 ActiveRecord::Relation
对象,这样就可以继续在这个对象上调用其他方法(如其他作用域)。
要想定义简单的作用域,我们可以在类中通过 scope 方法定义作用域,并传入调用这个作用域时执行的查询
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
end
?
class Article < ApplicationRecord
def self.published
where(published: true)
end
end
?
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
动态查找方法
Active Record 为数据表中的每个字段(也称为属性)都提供了查找方法(也就是动态查找方法)。例如,对于 Client
模型的 first_name
字段,Active Record 会自动生成 find_by_first_name
查找方法。对于 Client
模型的 locked
字段,Active Record 会自动生成 find_by_locked
查找方法。
在调用动态查找方法时可以在末尾加上感叹号(!
),例如 Client.find_by_name!("Ryan")
,这样如果动态查找方法没有返回任何记录,就会抛出 ActiveRecord::RecordNotFound
异常。
如果想同时查询 first_name
和 locked
字段,可以在动态查找方法中用 and
把这两个字段连起来,例如 Client.find_by_first_name_and_locked("Ryan", true)
。
enum宏
enum
宏把整数字段映射为一组可能的值
class Book < ApplicationRecord
enum availability: [:available, :unavailable]
end
?
# 下面的示例只查询可用的图书
Book.available
# 或
Book.where(availability: :available)
book = Book.new(availability: :available)
book.available? # => true
book.unavailable! # => true
book.available? # => false
方法链
有了方法链我们就可以同时使用多个 Active Record 方法
Person
.select('people.id, people.name, comments.text')
.joins(:comments)
.where('comments.created_at > ?', 1.week.ago)
?
SELECT people.id, people.name, comments.text
FROM people
INNER JOIN comments
ON comments.person_id = people.id
WHERE comments.created_at > '2015-01-01'
查找或创建新对象
查找记录并在找不到记录时创建记录,这时我们可以使用 find_or_create_by
和 find_or_create_by!
方法
Client.find_or_create_by(first_name: 'Andy')
# => #
?
SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT
?
假设我们想在新建记录时把 locked 字段设置为 false,但又不想在查询中进行设置。例如,我们想查找名为“Andy”的客户记录,但这条记录并不存在,因此要创建这条记录并把 locked 字段设置为 false
Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')
或者
Client.find_or_create_by(first_name: 'Andy') do |c|
c.locked = false
end
也可以使用 find_or_create_by!
方法,这样如果新建记录是无效的就会抛出异常。
find_or_initialize_by
方法的工作原理和 find_or_create_by
方法类似,区别之处在于前者调用的是 new
方法而不是 create
方法。这意味着新建模型实例在内存中创建,但没有保存到数据库。
使用SQL语句进行查找
要想直接使用 SQL 语句在数据表中查找记录,可以使用 find_by_sql
方法。find_by_sql
方法总是返回对象的数组,即使底层查询只返回了一条记录也是如此。
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER BY clients.created_at desc")
# => [
# #,
# #,
# ...
# ]
select_all
和 find_by_sql
方法一样,select_all
方法也会使用定制的 SQL 语句从数据库中检索对象,区别在于 select_all
方法不会对这些对象进行实例化,而是返回一个散列构成的数组,其中每个散列表示一条记录。
Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")
# => [
# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
# ]
pluck方法
pluck
方法用于在模型对应的底层数据表中查询单个或多个字段。它接受字段名的列表作为参数,并返回这些字段的值的数组,数组中的每个值都具有对应的数据类型。
Client.where(active: true).pluck(:id)
# SELECT id FROM clients WHERE active = 1
# => [1, 2, 3]
Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']
Client.pluck(:id, :name)
# SELECT clients.id, clients.name FROM clients
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
ids方法
可以获得关联的所有 ID,也就是数据表的主键。
Person.ids
# SELECT id FROM people
?
class Person < ApplicationRecord
self.primary_key = "person_id"
end
Person.ids
# SELECT person_id FROM people
检查对象是否存在
exists?
方法查询数据库的工作原理和 find
方法相同,但是 find
方法返回的是对象或对象集合,而 exists?
方法返回的是 true
或 false
。
单个值
Client.exists?(1)
?
多个值
Client.exists?(id: [1,2,3])
# 或
Client.exists?(name: ['John', 'Sergei'])
并且只要有一条对应记录存在就会返回 true
我们还可以在模型或关联上调用 any?
和 many?
方法来检查对象是否存在
# 通过模型
Article.any?
Article.many?
# 通过指定的作用域
Article.recent.any?
Article.recent.many?
# 通过关系
Article.where(published: true).any?
Article.where(published: true).many?
# 通过关联
Article.first.categories.any?
Article.first.categories.many?
计算
count
要想知道模型对应的数据表中有多少条记录
Client.count
# SELECT count(*) AS count_all FROM clients
average
要想知道数据表中某个字段的平均值
Client.average("orders_count")
minimum
要想查找数据表中某个字段的最小值
Client.minimum("age")
maximum
要想查找数据表中某个字段的最大值
Client.maximum("age")
sum
要想知道数据表中某个字段的所有字段值之和
Client.sum("orders_count")