备忘录模式
首先,让我们回答一个问题:
即使我使用了备忘录模式,为什么应用程序还会进行多个SQL调用?
是的,我们正在使用@policy ||= WorkPolicy.new
备忘录Policy对象。
但是我们没有备忘录对象调用的内容。这意味着我们需要备忘录基础对象方法调用结果。
因此,如果我们执行以下操作:
@work = Work.last
@work.policy.able_to_delete?
#=> SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 100] # sql call
@work.policy.able_to_delete?
#=> SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 100] # sql call
@work.policy.able_to_delete?
#=> SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 100] # sql call
...我们需要多次调用comments.count
但是如果我们引入另一层记忆化:
那么让我们改变这个:
class WorkPolicy
def work_has_no_comments
work.comments.count < 1
end
end
到这个:
class WorkPolicy
def work_has_no_comments
@work_has_no_comments ||= comments.count < 1
end
end
@work = Work.last
@work.policy.able_to_delete?
@work.policy.able_to_delete?
@work.policy.able_to_delete?
正如您所看到的,对计数的SQL调用仅在第一次进行,然后从对象状态的内存中返回结果。
缓存
但是,在我们“循环遍历多个工作”的情况下,这种方法行不通,因为我们正在使用100个WorkPolicy对象初始化100个Work对象
最好的理解方法是在irb
中运行此代码:
class Foo
def x
@x ||= calculate
end
private
def calculate
sleep 2
123
end
end
class Bar
def y
@y ||= Foo.new
end
end
p "10 times calling same memoized object\n"
bar = Bar.new
10.times do
puts bar.y.x
end
p "10 times initializing new object\n"
10.times do
bar = Bar.new
puts bar.y.x
end
解决这个问题的一种方法是使用Rails缓存
class WorkPolicy
def work_has_no_comments
Rails.cache.fetch [WorkPolicy, 'work_has_no_comments', @work] do
work.comments.count < 1
end
end
end
class Comment
belongs_to :work, touch: true
end
现在这只是一份愚蠢的示例。我知道可以通过引入Work#comments_count
方法并在其中缓存评论计数来解决这个问题。我只是想演示一些选项。
有了这样的缓存,第一次运行WorksController#index
时,我们将会进行多次 SQL 调用:
SELECT "works".* FROM "works"
SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 1]
SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 2]
SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 3]
SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 4]
# ...
但第二次、第三次通话将会是这样:
SELECT "works".* FROM "works"
# no count call
如果您向ID为3
的工作添加新评论:
SELECT "works".* FROM "works"
SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 3]
正确的SQL
现在我们还不满意。我们希望第一次运行能够快速!问题在于我们调用关联(评论)的方式是延迟加载:
Work.limit(3).each {|w| w.comments }
# => SELECT "works".* FROM "works" WHERE ORDER BY "works"."id" DESC LIMIT 10
# => SELECT "comments".* FROM "comments" WHERE "comments"."work_id" = $1 ORDER BY comments.created_at ASC [["work_id", 97]]
# => SELECT "comments".* FROM "comments" WHERE "comments"."work_id" = $1 ORDER BY comments.created_at ASC [["work_id", 98]]
# => SELECT "comments".* FROM "comments" WHERE "comments"."work_id" = $1 ORDER BY comments.created_at ASC [["work_id", 99]]
但是如果我们急切地加载它们:
Work.limit(3).includes(:comments).map(&:comments)
SELECT "works".* FROM "works" WHERE "works"."deleted_at" IS NULL LIMIT 3
SELECT "comments".* FROM "comments" WHERE "comments"."status" = 'approved' AND "comments"."work_id" IN (97, 98, 99) ORDER BY comments.created_at ASC
阅读更多关于
http://blog.scoutapp.com/articles/2017/01/24/activerecord-includes-vs-joins-vs-preload-vs-eager_load-when-and-where中的
includes
和
joins
。
因此,我们的代码可能是这样的:
class WorksController < ApplicationController
def index
@works = Work.all.includes(:comments)
end
end
class WorkPolicy
def work_has_no_comments
work.comments.size < 1
end
end
问:等一下,comments.count
和commets.size
不是一样的吗?
并不完全相同。
10.times do
work.comments.size
end
# SELECT "comments".* FROM "comments" WHERE "comments"."work_id" = $1 ORDER BY comments.created_at ASC [["work_id", 1]]
...将所有评论加载到(类似)数组中,并对大小进行数组计算(好像[].size一样)
10.times do
work.comments.count
end
# SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 1]]
# SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 1]]
# SELECT COUNT(*) FROM "comments" WHERE "comments"."work_id" = $1 [["work_id", 1]]
# ...
...执行 SELECT COUNT
比加载“所有评论”以计算大小要快得多,但是当您需要执行此操作10次时,您明确地进行了10次调用
现在我夸大了 work.comments.size
Rails 更聪明地确定是否只想要 size
。 在某些情况下,它只执行 SELECT COUNT(*)
而不是“将所有评论加载到数组” 并执行[].size。
这很类似于.pluck
与.map
scope = Work.limit(10)
scope.pluck(:title)
scope.pluck(:title)
scope.map(&:title)
scope.map(&:title)
pluck
只选择 title
到数组,所以速度更快,但每次都会执行 SQL 查询
map
会导致 Rails 对 SELECT *
进行评估以填充 title
到数组,但然后您可以使用已加载的对象进行操作
结论
没有银弹。它总是取决于您想要实现什么。
有人可能会认为“优化 SQL”解决方案效果最佳,但这并不正确。您需要在每个调用 work.policy.able_to_delete
的地方实现类似的 SQL 优化,这可能是 10 或 100 个位置。从性能角度来看,includes
并不总是一个好主意。
缓存可能在事件级别上被超级链接,以确定哪个事件应该删除缓存的哪个部分。如果您没有正确执行此操作,则您的网站可能会显示“过时信息”!对于策略对象而言,这是非常危险的。
记忆化并不总是足够灵活,因为您可能需要重新设计大量代码库才能实现它,并引入过多的不必要的抽象层
更不用说记忆化在像 Rubinius 这样的线程安全环境中是大忌,除非您正确同步线程。如果您使用 MRI,则可以放心使用记忆化(在 95% 的情况下)。Rails 和 Puma 都是线程安全的,但那是一种不同类型的“线程安全”。您真的需要做一些愚蠢的事情才会成为问题。这篇文章太长了,无法涉及该主题。请谷歌它!
具体要看你的应用程序(应用程序的某个部分)的目标是什么。我的唯一建议是:对你的应用程序进行配置文件/基准测试!不要过早地进行优化。使用New relic等工具发现你的应用程序中哪些部分较慢。
逐步优化,不要构建缓慢的应用程序然后决定"好吧,让我们进行优化",因为你可能会发现自己做出了糟糕的设计选择,而你的App需要重写以提高速度。
其他未提到的解决方案
计数器缓存
数据库索引
听起来可能不相关但很多性能问题是因为你的应用程序没有DB索引(或过多的预发布索引)导致的。