Gem 分享一个新做的 gem:smart_collection

cicholgricenchos · 发布于 2018年01月10日 · 最后由 cicholgricenchos 回复于 2018年01月10日 · 487 次阅读
96

Github Repo

在电商环境中,我们常常需要根据一些条件去选择一组商品,例如从类目A和类目B里选出价格少于10元的产品。而且我们希望当某个产品涨价大于10元的时候,会自动退出这个collection。

这是shopify的一个卖点:智能类目,我把它做成了一个AR的插件,让其他Rails项目也能实现类似的功能。

我们可以这样去定义一个Collection Model:

class CreateCollections < ActiveRecord::Migration[5.0]
  def change
    create_table :collections do |t|
      t.text :rule
      t.datetime :cache_expires_at
      t.timestamps
    end
  end
end

class Collection < ActiveRecord::Base
  serialize :rule, JSON

  include SmartCollection::Mixin.new(
    items: :products
  )
end

创建一个collection:

collection = Collection.create(
  rule: {
    and: [
      {
        or: [
          {
            association: {
              class_name: 'Catalog',
              id: @pen_catalog.id,
              source: 'products'
            }
          },
          {
            association: {
              class_name: 'Catalog',
              id: @pencil_catalog.id,
              source: 'products'
            }
          }
        ]
      },
      {
        condition: {
          joins: 'properties',
          where: {
            properties: {
              value: 'Red'
            }
          }
        }
      }
    ]
  }
)

选出商品:

collection.products #=> 选出水笔和铅笔中有“红色”属性的产品
collection.products.where(in_stock: true).order(id: :desc) #=> 关联返回的仍旧是scope,可以继续进行where order等操作

实现原理是自定义一个association,用条件拼出来的scope重载掉association_scope,因为rails5里scope可以or,用merge和or拼接很方便。condition子句原样支持AR的joins和where参数,我发现这样的表达力是最好的,就不自己造其他dsl了。

这时直接去用可能会比较慢,因为每个集合的scope都需要一条查询,如果批量会有n+1问题,我们需要使用缓存。

smart_collection提供了table缓存和cache_store缓存,启用之后就可以使用preload了,如果缓存过期,单个读取和preload都会更新缓存。

启用缓存:

class CreateSmartCollectionCachedItems < ActiveRecord::Migration[5.0]
  def change
    create_table :smart_collection_cached_items do |t|
      t.integer :collection_id
      t.integer :item_id
    end
  end
end

class Collection < ActiveRecord::Base
  serialize :rule, JSON

  include SmartCollection::Mixin.new(
    items: :products,
    cached_by: {
      table: :default,
      expires_in: 1.hour
    }
    #cached_by: {
    #  cache_store: Rails.cache,
    #  expires_in: 1.hour
    #}
  )
end


Collection.where(id: [1, 2]).preload(products: :properties) #=> 不会有n+1问题

隐藏特性:对于table缓存,eager_load也是可行的,但是eager_load的时候没法判断collection的缓存是否过期,所以只适合定期更新全部缓存的场景。

缺陷:在rule里存储的数据,如果源数据被删掉了,就会出错,未来考虑把collection依赖的数据都保存起来,实现dependent: :destroy之类的特性。

共收到 2 条回复
8

🌟 🌟 🌟 🌟 🌟

用table缓存相当于把items静态化到一张表里,但更新需要自己维护? 不用table缓存是完全动态根据rule查?

96
8hooopo 回复

是的,用缓存的话等过期就好,读取和preload会更新缓存,不用手动

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