25 May 2015

前言

有个需求,需要筛选出用户,从而发送邮件。

正文

那条筛选语句,最初是这样的:

User.includes(:trades).where("last_sign_in_at <= ? and last_sign_in_at >= ?  and trades.id is not null ", 1.weeks.ago, 8.days.ago)

后来要加上 7 天, 30 天, 以及 180天,简单的扩展一下,变成这样:

User.includes(:trades).where("(last_sign_in_at <= ? and last_sign_in_at >= ?) or (last_sign_in_at <= ? and last_sign_in_at >= ?) or (last_sign_in_at <= ? and last_sign_in_at >= ?)  and trades.id is not null ", 7.days.ago, 8.days.ago, 30.days.ago, 31.days.ago, 180.days.ago, 181.days.ago)

使用sql的between语句估计还要更长。 为了减少写这么可怕的语句,将查询分为3次:

def deliver_mail(time)
  User.includes(:trades).where("last_sign_in_at <= ? and last_sign_in_at >= ?  and trades.id is not nul ", time.days.ago,  (time+1).days.ago) 
end

[7, 30, 180].each { |x| deliver_mail(x) }

但是,查一次肯定比查三次要好。这时,使用数据库提供的date函数,语句就短一些:

User.includes(:trades).where("date(last_sign_in_at) in (?) and trades.id is not null" , [7.days.ago.to_date, 30.days.ago.to_date, 180.days.ago.to_date])

上述的语句,如果使用的地方很多,就可以使用scope封装。 在User模型中,写入如下的scope:

class User < ActiveRecord::Base
  # 查询在特定日期内登录过的用户
  scope :last_sign_date, ->(*time) { where("date(last_sign_in_at) in (?)" ,time.map {|d| d.to_date } ) }
  scope :has_live , includes(:live_account).where("live_accounts.user_id is not null")
  scope :no_vt_and_no_live, includes(:trades, :live_account).where("trades.id is null and live_accounts.user_id is null")
  scope :has_vt_but_no_live, includes(:trades, :live_account).where("trades.id is not null and live_accounts.user_id is null")
end

使用scope之后,上面的查询语句可以简化成如下语句,可读性变高了:

User.no_vt_and_no_live.last_sign_date(7.days.ago, 30.days.ago, 180.days.ago)

多次使用scope,尝试代码块的代码:

#encoding: utf-8
class InvalidEmailsController < ApplicationController
  inherit_resources

  def index
    @invalid_emails = InvalidEmail.page(params[:page]).per(20)
    if @invalid_emails.current_page == 1
      @callback_user_count = {} 
      @callback_user_count["无模拟无实盘"] = get_callback_user_count { User.no_vt_and_no_live }
      @callback_user_count["有模拟没实盘"] = get_callback_user_count { User.has_vt_but_no_live }
      @callback_user_count["有实盘"] = get_callback_user_count { User.has_live }
    end
  end
  
  protected
    def get_callback_user_count(&blk)
      arr = {}
      # 我原本是这样写的,[7.days.ago, 30.days.ago, 180.days.ago],后来发现很脑残
      # 违反了最小知识
      [7, 30, 180].each do |time|
        arr.store("#{time}天", blk.call.last_sign_date(time.days.ago).count)
      end
      arr
    end
end

备注: 可以看到 get_callback_user_count 中查询了三次数据库,如果,可以一次将数据查出来,那该多好。听前辈说,group可以做到,不管怎么样, sql方面,我不是很熟,可能需要自己加强一下。

总结,ActiveRecord中,可直接写sql语句,其提供的语法简化了sql语句。 抽象问题的水平,决定了编码的水平。

直接使用数据库提供的函数,可能会形成对DBRMS的依赖。

参考文献

  1. Rails ActiveRecord date between



傲娇的使用Disqus