Gem 使用 associationist 玩转 Rails 虚拟关联

cicholgricenchos · 2019年05月26日 · 最后由 jasl 回复于 2019年06月26日 · 1540 次阅读
本帖已被设为精华帖!

Github Repo 中文文档

一般来说Rails的关联是要在数据表里通过外键实现的,但是有时候会有一些形式上是关联的数据,却没法通过数据表实现。

例如我们想在一个保存为text字段的帖子中,查询所有提到的人,并且使用includes一并加载出来,也就是我们想:

Post.first.mentioned_people # => 所有被提到的Person
Post.includes(:mentioned_people).all

往常我们是做不到的,因为mentioned_people和post之间并没有真正存在的关联。这时候我们可以用associationist虚拟出一个自定义的关联,具体代码是这样的:

class Post < ApplicationRecord
  include Associationist::Mixin.new(
    name: :mentioned_people,
    type: :collection,
    scope: -> post {
      Person.where(id: post.extract_mentioned_ids)
    }
  )

  def extract_mentioned_ids # 返回一个id数组
    content.scan(/提取id的模式/).map(&:first).map(&:to_i)
  end
end

现在直接在post对象上调用已经可以正确取出关联了,并且可以像往常一样在之后叠加limit, count等方法。

Post.first.mentioned_people
Post.first.mentioned_people.limit(1)
Post.first.mentioned_people.count

不过只定义了scope的情况,在preload的时候还是会有n+1问题,因为active record的preloader还不知道怎么批量加载这些关联,需要我们自己定义:

class Post < ApplicationRecord
  include Associationist::Mixin.new(
    name: :mentioned_people,
    type: :collection,
    scope: -> post {
      Person.where(id: post.extract_mentioned_ids)
    },
    # preloader需要返回一个key为对象,value为关联数据的hash
    preloader: -> posts {
      people_ids = posts.map(&:extract_mentioned_ids).inject(:+)
      people_hash = Person.where(id: people_ids).map{|person| [person.id, person]}.to_h
      posts.map do |post|
        [post, post.extract_mentioned_ids.map{|id| people_hash[id]}]
      end.to_h
    }
  )
end

这个preloader做的事就是将一组post关联的people id先取出来,然后使用一次sql查询查出,再安装回post上。

现在includes方法也可以正常使用了,并且可以像往常一样添加多级的includes:

Post.includes(:mentioned_people).all
Post.includes(mentioned_people: :address).all

事实上可以定义成虚拟关联的不只scope,可以是任何对象:

class Product < ApplicationRecord
  include Associationist::Mixin.new(
    name: :stock,
    preloader: -> products {
      products.map{|product| [product, 1]}.to_h
    }
  )
end

Product.first.stock #=> 1

这样我们可以把一些复杂sql甚至不是sql的东西抽象成关联。事实上我设计associationist的初衷,就是想让一个很复杂的库存查询的取用变简单。

目前associationist只提供了最基础的虚拟关联,其实还可以更进一步去提供一些操作collection proxy的方法等等,这么做可以让active record真正变成多数据源的,不仅仅能用sql。(不过没需求就算了)

顺便一提,我之前做的用来实现shopify的智能类目的gem:https://ruby-china.org/topics/34865, 已经改为基于associationist实现。 smart_collection里面的scope是不定的,对于每个条目都可能生成不同的scope,所以preloader没有这个Post的这么好写,要额外用到一个cache store或者cache table,有需要的可以参考参考。

共收到 8 条回复
jasl 将本帖设为了精华贴 05月26日 17:59

有意思

水桥还是有意思 。你说的 Preloader 其实就是用 in 查询解决。记得加 Index 不然巨慢

你泯灭了了的

这个完善一下如果能集成到rails里,对复杂项目还是有很大帮助的,比一些语法糖之类的feature要有用的多

hooopo 回复

嗯,在打理一下可以发个feature request看看他们喜不喜欢

Association 的 Preloader 挺复杂的,虚拟关联也是刚需,之前我尝试实现虚拟关联(用 Association 来实现虚拟关联的),但是工作量太大就放弃了

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