Shopping Cart Goods Count Update in Rails(class variable)

总结一下在电商站点中(Rails),后台更新购物车数量的实现。

功能分析

因为购物车的数量是显示在顶部导航栏内,任何一次访问页面,都会涉及到此数据的显示和更新问题。主要实现需要放在application_controller中。

先说显示问题,显然每次都要显示,意味着在任何的controller中都要有此数据,这就需要一个全局的变量来存储这个数据。 全局变量的选择,可以是类变量,@@开头的变量,或者是$开头的变量,该变量设置在顶级controller中,也即是application_controller 中。

更新问题的话,初始化设置一次,这里还有一个问题,什么时候会初始化application_controller中非函数的部分? 后续执行并不需要每次都更新,只有在购物车改变时需要更新,也就是carts_controller中的删除、创建、更新这三个action需要更新。 这就是说,在这三个action执行以后,application_controller需要知道购物车已经更新,数目需要重新计算。这就需要一个通知机制, 这里也使用一个全局变量。

还有,要在前端view中显示,需要一个实例变量来存储,也就是一个@开头的变量。

实现代码

application_controller实现,这里只有本文相关代码,其他代码略去。这里使用一个字典来缓存相应用户的数据。 key为用户的id,value为一个结构体,此结构体有两个字段,一个是缓存的计数count,一个记录是否购物车做过改变is_changed

class ApplicationController < ActionController::Base
  before_action :set_cart_num

  # for cache num,
  CartCount = Struct.new(:count, :is_changed)
  # dict, {user_id => CartCount, ... }
  @@cached_cart = {}

  def set_cart_num
    if current_user
      id = current_user.id
      # exist
      if @@cached_cart[id]
        if @@cached_cart[id].is_changed
          # changed
          @cart_num = @@cached_cart[id].count = count_carts
          @@cached_cart[id].is_changed = false
        else
          # "no change"
          @cart_num = @@cached_cart[id].count
        end
      else
        # "not exist"
        @@cached_cart[id] = CartCount.new(count_carts, false)
        @cart_num = @@cached_cart[id].count
      end
    end
  end

  def count_carts
    current_user.carts.collect{|item| item.amount}.sum
  end
  # 在涉及到购物车修改的地方,都需要调用此方法,以刷新缓存。
  # 例如在cart_controller和orders_controller中
  def notify_cart_change
    @@cached_cart[current_user.id].is_changed = true
  end

end

carts_controller实现,carts_controller主要维护购物车中的条目,每一个条目是一种商品及其数量。

class CartsController < ApplicationController
  after_action :notify_cart_change, only: [:create, :update, :destroy]
end

orders_controller中也要调用刷新缓存的方法

class CartsController < ApplicationController
  after_action :notify_cart_change, only: [:create, :update, :destroy]
end

view

<a href="/carts"><img src="/assets/shopping-cart1.png" class="" alt="carts">
  <span class="badge"><%= @cart_num %></span>
</a>

schema of carts

create_table "carts", id: :serial, force: :cascade do |t|
  t.integer "user_id"
  t.integer "product_id"
  t.integer "amount"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["product_id"], name: "index_carts_on_product_id"
  t.index ["user_id"], name: "index_carts_on_user_id"
end

疑问与思考

思考1: 该缓存@@cached_cart = {}对象,是一个全局对象,会随着用户的增多而不断增大,后面用户量变大,需要考虑设置过期时间,或者将该数据结构保存在外置的内存数据库中,如redis等。可以在用户登出以后,销毁该用户的缓存数据

# routes.rb
devise_for :users, controllers: {
  :passwords => 'users/passwords',
  :registrations => 'users/registrations',
  :sessions => 'users/sessions'
}
# sessions_ontroller.rb

class Users::SessionsController < Devise::SessionsController
  before_action :clear_cart_count, only: [:destroy]
  private
  # after user sign_out, delete the cached cart count.
  def clear_cart_count
    @@cached_cart.delete current_user.id
  end
end

思考2: 使用结构体是否会占用内存过多?替换成数组是否会更节省内存? 毕竟只有两个字段,使用结构体是为了清晰。 我们来对比测试一下,同样是两个字段,用数组和结构体分别使用的内存。

require 'objspace'
CartCount = Struct.new(:count, :is_changed)
c = CartCount.new(12, false)
p ObjectSpace.memsize_of(c)
# => 40
d = [13, false]
p ObjectSpace.memsize_of(d)
# => 40

从上面可以看出,无论是用结构体还是数组,使用内存均为40。 关于整个application_controller的初始化问题,什么时候会初始化application_controller中所有内容?在程序启动初始化时。