Rails AR 有办法将一个查询虚拟成 has_one 关联吗?(答案是可以)

mizuhashi · 发布于 2016年12月19日 · 最后由 mizuhashi 回复于 2016年12月21日 · 479 次阅读
23529

场景:

原来model里有product has_one review_summary,review_summary是一个reviews的统计数据,目前由定时任务以及回调更新。
现在这个summary的逻辑简化了,可以通过一个简单的查询查出来,所以想将原来的has_one关联替换成一个查询,这样可以沿用之前includes的代码,逻辑上也是一致的。

我知道视图可以做这个,但是希望可以在ruby的层面上做这个事情,不知可有好的方法?

共收到 6 条回复
18898
flemon1986 · #1 · 2016年12月20日

没太清楚这场景,但是has_one 貌似必须有个foreign_key吧。。。写个方法调用吧。。。

23529
mizuhashi · #2 · 2016年12月20日

#1楼 @flemon1986 foreign_key有的,例如select products.id as product_id, avg(stars) as avg_star from products join reviews,可以把products_id作为这个表的外键

18898
flemon1986 · #3 · 2016年12月20日
has_one :review_summary, -> { where(is_default: true) }

是这种吗?

23529
mizuhashi · #4 · 2016年12月21日 1 个赞

#3楼 @flemon1986 我找到办法了

class Product
  has_many :reviews
  has_one :review_summary
end

class ReviewSummary < ApplicationRecord
  self.table_name = 'reviews'
  default_scope {
    from('products').
      joins('INNER JOIN reviews ON reviews.product_id = products.id').
      select('products.id as product_id, avg(stars) as avg_star')
  }

end

然后

p = Product.includes(:review_summary).first
p.review_summary.avg_star # => 平均评分

这样就可以利用includes把统计数据都查出来了,生成的查询是这样的

SELECT products.id as product_id, avg(stars) as avg_star FROM products INNER JOIN reviews ON reviews.product_id = products.id WHERE "reviews"."product_id" IN (1, 2)

上面是用reviews.product_id用来做where条件的,可能有点巧合的感觉,一种更好的配法是

class ReviewSummary < ApplicationRecord
  self.table_name = 'products'

  default_scope {
    joins('INNER JOIN reviews ON reviews.product_id = products.id').
      select('products.id as id, avg(stars) as avg_star')
  }

end

class Product < ApplicationRecord
  has_many :reviews
  has_one :review_summary, foreign_key: :id
end

生成的查询是:

SELECT products.id as product_id, avg(stars) as avg_star FROM products INNER JOIN reviews ON reviews.product_id = products.id WHERE "products"."id" IN (1, 2)

用products.id来写where正是我们想要的行为,完美。。这样虚拟出关联还是相当好玩的,不用create view了

18898
flemon1986 · #5 · 2016年12月21日
has_one :review_summary,
  -> { joins('INNER JOIN reviews ON reviews.product_id = products.id').select('products.id as id, avg(stars) as avg_star')},
  class_name: "Product"

这样?

23529
mizuhashi · #6 · 2016年12月21日

#5楼 @flemon1986 应该也行,还差个foreign_key,只是用Product还是用ReviewSummary来实例化的问题,反正avg_star肯定读的到的

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