Rails 这算不算 counter_cache 的设计问题?

jasl · 2013年01月19日 · 最后由 jasl 回复于 2013年01月19日 · 3218 次阅读

见代码说话

[1] pry(main)> Node.last.posts
  Node Load (0.2ms)  SELECT `nodes`.* FROM `nodes` ORDER BY `nodes`.`id` DESC LIMIT 1
  Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`node_id` = 5
=> []
[2] pry(main)> Post.first.node = Node.last
  Node Load (0.1ms)  SELECT `nodes`.* FROM `nodes` ORDER BY `nodes`.`id` DESC LIMIT 1
  SQL (0.3ms)  UPDATE `nodes` SET `posts_count` = COALESCE(`posts_count`, 0) + 1 WHERE `nodes`.`id` = 5
  SQL (0.3ms)  UPDATE `nodes` SET `posts_count` = COALESCE(`posts_count`, 0) - 1 WHERE `nodes`.`id` = 1
=> #<Node id: 5, name: "ttt", cover: nil, description: nil, created_at: "2013-01-18 21:34:14", updated_at: "2013-01-18 22:15:41", posts_count: 0, state: "publish">
[3] pry(main)> Post.first.node
=> #<Node id: 1, name: "System", cover: nil, description: "", created_at: "2012-12-07 18:29:00", updated_at: "2012-12-07 18:40:03", posts_count: 4, state: "system">
[4] pry(main)> Node.last.posts_count
  Node Load (0.4ms)  SELECT `nodes`.* FROM `nodes` ORDER BY `nodes`.`id` DESC LIMIT 1
=> 1
[5] pry(main)> Node.last.posts
  Node Load (0.4ms)  SELECT `nodes`.* FROM `nodes` ORDER BY `nodes`.`id` DESC LIMIT 1
  Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`node_id` = 5
=> []
[6] pry(main)> Node.last.posts_count
  Node Load (0.4ms)  SELECT `nodes`.* FROM `nodes` ORDER BY `nodes`.`id` DESC LIMIT 1
=> 1

在给 post 的 node 赋值时 counter 便发生 update,但此时 post 不一定会被 save,这样就造成了不一致性,此外,更新 node_id 时不会更新 counter 阅读了源码,发现似乎就是这么设计的,并且在单元测试中也是按照这个逻辑去做的。

十分不解这样设计的目的,为什么不在更新之后更新 counter?我写了一个 patch,正在犹豫要不要 pr

看的太累了。以后不如来个 mock testing,看的快。你提交个 patch 看看,犹豫啥。

自从发现 counter_cache 的行为有时不符合我理解之后,我就自己写 after_create 了。

会不会是为了 save 前用到 count 可以保证逻辑上的 count 数?

Post.first.node = Node.last 这句的行为在 Rails 2 的时候好像就已经是这样了 要做关联的话,记得应该是用 Node.last.build_post() 然后再 save

你 reload 试下,看看 counter 有没有变化

@Ddl1st 不会 @edokeh 有这么一种场景,比如 post 属于 node1,然后把他移动到 node2 了,然后 counter 就乱掉了

@xds2000 开了一个 issue 看看

#6 楼 @jasl 我觉得没什么不妥,只是临时的更新了变量而已,如果没有保存自动释放'池'

@Ddl1st 当然不是临时的。。。counter 的改动直接就写入数据库了,但是资源却不一定被持久化,这当然是问题

#9 楼 @jasl 你加了 autosave ?

@Ddl1st 没有啊,他就是这个行为,你读读源码就知道了

@hysios 从源码的逻辑看,确实写入数据库了,而且很有可能造成最终一致性问题 ar 的策略是 如果存在 counter_cache 那么在求 size 的时候会直接取 counter

15 楼 已删除

@hysios 如果设置了 counter_cache 那么多对一的一端要求有一个字段来缓存多端的 size,这个字段是 rails 来管理的,在对多端进行 count/size 查询的饿时候 rails 会直接取这个字段 避免了 count(*) 操作

#16 楼 @jasl 好像是这样的,你如果要修改关联,要先删除旧关联(设置为 nil),然后再赋值,这样两边才会都对

@edokeh 这个字段是 rails 管理的,所以他不应该出现我描述的这种情况,除非有什么设计上的考量。 先删除就关联 再赋值 一样会有问题 因为赋值之后 counter 立即更新并持久化了 那我因为一些原因不把 resource 保存了呢?counter 还是会错的

写个 unit test 吧,看得真心累

#18 楼 @jasl 恩,这里确实有些奇怪,不过也可能是我们没有摸清 DHH 大神的设计意图 这会儿手头没环境,痛苦,回头找台机好好看看

需要 登录 后方可回复, 如果你还没有账号请 注册新账号