除了页面(Page), 动作(Action), 片段(Fragment)等这三种 Rails 所支持的缓存以外, Rails 还提供了更底层的, 对于特定值或者查询结果缓存的支持.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  | 
[3] pry(main)> Rails.cache.read('first_post_cache')
=> nil
[5] pry(main)> Rails.cache.write('first_post_cache', Post.first, expires_in: 2.minute)
  Post Load (0.3ms)  SELECT  `posts`.* FROM `posts`  ORDER BY `posts`.`id` ASC LIMIT 1
=> true
[6] pry(main)> Rails.cache.read('first_post_cache')
=> #<Post:0x007fcde2587d40
 id: 1,
 title: "Rails.cache.fetch scope 的'坑'",
 body: '',
 created_at: Fri, 03 Jan 2015 23:55:44 CST +08:00,
 updated_at: Thu, 21 Jul 2015 08:00:20 CST +08:00>
  | 
 
每次都要先 read 再 write 比较繁琐, 所以, 就有了一个方便些的方法 Rails.cache.fetch
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  | 
[7] pry(main)> Rails.cache.fetch('cache_first_post', expires_in: 2.minute) { Post.first }
  Post Load (0.4ms)  SELECT  `posts`.* FROM `posts`  ORDER BY `posts`.`id` ASC LIMIT 1
=> #<Post:0x007fcde2474fe8
 id: 1,
 title: "Rails.cache.fetch scope 的'坑'",
 body: ''>
 [9] pry(main)> Rails.cache.fetch('cache_first_post', expires_in: 2.minute) { Post.first }
 => #<Post:0x007fcde228afc0
  id: 1,
  title: "Rails.cache.fetch scope 的'坑'",
  body: '',
  created_at: Fri, 03 Jan 2015 23:55:44 CST +08:00,
  updated_at: Thu, 21 Jul 2015 08:00:20 CST +08:00>
  | 
 
咋看上去都还正常运作的, Rails.cache.fetch 在对应的 key 如果能找到值的时候就不会再去查数据库了.
再来看下面,
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  | 
[10] pry(main)> Rails.cache.fetch('cache_all_posts', expires_in: 2.minute) { Post.all }
  Post Load (4.8ms)  SELECT `posts`.* FROM `posts`
=> [#<Post:0x007fcddc2dfc38
  id: 1,
  title: "Rails.cache.fetch scope 的'坑'",
  body: '',
  created_at: Fri, 03 Jan 2015 23:55:44 CST +08:00,
  updated_at: Thu, 21 Jul 2015 08:00:20 CST +08:00>,
  #<Post:0x007fcddfab0e28
  id: 2...]>
[11] pry(main)> Rails.cache.fetch('cache_all_posts', expires_in: 2.minute) { Post.all }
  Post Load (4.1ms)  SELECT `posts`.* FROM `posts`
=> [#<Post:0x007fb96b6a3c50
  id: 1,
  title: "Rails.cache.fetch scope 的'坑'",
  body: '',
  created_at: Fri, 03 Jan 2015 23:55:44 CST +08:00,
  updated_at: Thu, 21 Jul 2015 08:00:20 CST +08:00>,
  #<Post:0x007f9a7fd787e8
  id: 2,
  | 
 
即便第一次看起来做了缓存(其实也的确有缓存文件在 tmp/cache/ 下面), 但是, 当再次执行 Rails.cache.fetch 的时候, 还是去数据库查了. 实际使用中可能不会这么干把所有 posts 都缓存(Redis/Memcached)到内存里面, 那样的话供给缓存用的内存估计瞬间就被榨干了. 但是, 这里数据库又再去查了一次感觉不科学啊.
搜了下 Stack Overflow, 说是, User.where('status = 1').limit(1000) Post.all 这些, 返回的实际上是 scope, 并非真正的查询. cache 的是 scope 不是查询结果.
换成
Rails.cache.fetch('cache_all_posts', expires_in: 2.minute) { Post.all.load }
以后, 就跟预想的一样工作了. 但是, 查看(ActiveSupport::Cache::FileStore)了一下缓存文件, 当缓存 scope 时候, 文件大小为 680 bytes, 而当缓存查询结果的时候, 文件的大小是 MB 级别的.
ActiveRecord::Relation 究竟是什么鬼?
1
2
3
  | 
[7] pry(main)> User.where(id: 1)
  User Load (6.9ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
=> [<User:0x007fb96ee98b88 id: 1, email: "test@test.com">]
  | 
 
看上去返回的结果很像是数组, 实际上, 却不是数组.
1
2
3
  | 
[8] pry(main)> _.class
=> User::ActiveRecord_Relation(Rails 4)
=> ActiveRecord::Relation(Rails 3)
  | 
 
btw, Rails 3 中执行 all 是返回 Array的, 而 Rails 4 则是 Relation
ActiveRecord::Relation 只有当真正需要知道并使用到里面所包含的对象时候才会被执行. 比如在 controller 中:
1
2
3
4
5
6
7
  | 
class PostsController < ApplicationController
  def index
    @posts    = Post.all
    @channels = Channel.all
    @comments = Comment.all
  end
end
  | 
 
假设在 index.html.erb 中, 没有任何地方用到 @comments 的话, 其实是不会执行数据库查询去把所有 comments 抓出来的. 而如果先使用到了 @channels, 比如 @channels.first.name 的话, 也是一样, 先执行
1
  | 
  Channel Load (27.8ms)  SELECT `channels`.* FROM `channels`
  | 
 
再去执行(假设 view 里面有使用到 @posts)
1
  | 
  Post Load (47.2ms) SELECT `posts`.* FROM `posts`
  | 
 
所以, Rails.cache.fetch('cache_all_posts', expires_in: 2.minute) { Post.all } 缓存的只是 Relation 对象而不是查询结果. 其实分开来, 使用 Rails.cache.read 和 Rails.cache.write 也是这样的.  ╮(╯_╰)╭ 说是坑, 好吧, 其实还是没透彻地弄清楚 Rails