分享 几个鲜为人知的 ActiveRecord Associations 小技巧 (译)

algo31031 · 2014年07月04日 · 最后由 debugger 回复于 2014年07月04日 · 2875 次阅读

原文出自gotealeaf.com, 感谢作者 Steve Turczyn

作为一个 Rails 开发者, 与 ActiveRecord associations 打交道实属家常便饭. 但其中若干特性, 却非尽人皆知.

自定义查询

假如你在开发一个允许发表评论的博客, 保不齐会遇到各种不和谐的言论充斥于评论间 (这里毕竟是互联网). 为了保证只显示经核准的评论, 你要在 comments 表里加一个boolean型字段:approved.

对于每篇博客, 由于只想取出审核过的评论 (即那些被管理员标记为通过的), 所以需要自定义查询.

# app/models/post.rb
class Post < ActiveRecord::Base
  has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
  ...

现在, 当你需要取出某篇博客里审核过的评论时, 只需要使用my_post.approved_comments.

扩展

有可能你还是需要有个选项能调出一篇博客的全部评论, 或者只看在今天发的那些. 这时你可以利用扩展功能, 在某个特定的关系上追加一个自定义方法.

# app/models/post.rb
class Post < ActiveRecord::Base
  has_many :comments do
    def today
      where("created_at >= ?", Time.zone.now.beginning_of_day)
    end
  end
  ...

这样一来, 除了原本的my_post.comments, 你还能通过诸如my_post.comments.today这种调用让检索更确切.

回调

在记录被添加或移除之前与之后, 自动的调用方法, 这个可以有.

假如你有一个建筑项目 (BuildingProject), 里面有许多工人 (Worker). 每次增加一个工人, 都需要重新计算项目预算和完工日期的变化, 而这些有又可能与工人类型工作经验等等息息相关...好个麻烦的方法, 要能在添加完工人后自动调用就好了.

# app/models/building_project.rb
class BuildingProject < ActiveRecord::Base
  has_many :workers, after_add: :recalculate_project_status
  ...
  def recalculate_project_status(newly_added_worker)
    ...
  end
  ...

原文出自gotealeaf.com, 感谢作者 Steve Turczyn

inverse_of

inverse_of可以很方便的使相互关联的对象从任意方向被访问到. my_post.commentsmy_comment.post. 当然, 在Post里指定has_many :comments并且在Comment里指定belongs_to :post之后, 便已经可以得到上述 2 个关系. 但是加上inverse_of之后, 会让 rails 确保关系里的对象乃是相同的对象.

inverse_of指定方法如下:

# app/models/post.rb
class Post < ActiveRecord.Base
 has_many :comments, inverse_of: post
 ...

现在先去掉inverse_of, 如果你这样这样这样...

post = Post.first
post.update_attribute(:importance, false)
comment = post.comments.first
working_post = comment.post
working_post.update_attribute(:importance, true)
post.importance?
=> false

喔! 系统咋还是觉得你的 blog 不给力...这是因为comment.post执行了一次单独的 DB 查询, 得到的对象虽然对应 DB 里同一条 Post 记录,但却是不同的实例. 而原本那条 post 记录的对象还是不知道自己在 DB 的状态已经被改变了.

那么让我们把inverse_of加回去, 重新执行刚才的代码:

post = Post.first
post.update_attribute(:importance, false)
comment = post.comments.first
working_post = comment.post
working_post.update_attribute(:importance, true)
post.importance?
=> true

好多了! 加上inverse_of之后, comment.post不再做 DB 查询, 而是直接使用已被加到内存里的 post 对象的实例. 换言之, 这时的working_post就是上面的post, 所以对working_post的改变会直接反应在post上.

相比引用博客所说的这个

has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'

我更喜欢这样

class Post < ActiveRecord::Base
  def approved_comments
    comments.approved
  end
end

第一个做法,Post 了解 Comment 太多了,直接指定到了 Comment 表的字段。

第二个做法,Post 只接触 Comment 给予的 API, 至于是approved: true还是approved: 'pass'还是approved: 1还是任何其他,由 Comment 自己决定。

我比较糙, 一般只在 Comment 里放一堆 scope 就满足了...

刚才顺便看了眼 api, 原来 scope 里也可以像 has_many 那样加扩展

class Shirt < ActiveRecord::Base
  scope :red, -> { where(color: 'red') } do
    def dom_id
      'red_shirts'
    end
  end
end

楼主的分享挺不错的 #1 楼 @billy 我也是你这样的做法,不过那是因为我一直都不知道楼主在这里陈述的方法

不够"鲜" 😄

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