Rails 拒绝自己拼凑查询条件

martin · 2014年10月28日 · 最后由 ThxFly 回复于 2019年03月29日 · 3321 次阅读

1. 场景

最近接手一个代码,流程同我们平时看到的一样:

  • 前段输入参数并提交
  • controller 判断并转 model 处理
  • model 查询返回结果

这样就导致每个 controller/model 充斥了不同的判断条件和查询的组装,如

result = Booking.where(corp_id: corp_id).includes(:member,:coach,:training_item)
result = result.where(coach_id: coach_id) if coach_id.present?

result = result.where(member_id: member_id) if member_id.present?
result = result.where(training_item_id: training_item_id) if training_item_id.present?
result = result.where(train_status: train_status) if train_status.present?
result = result.where("name like ?",'%#{member_name}%') if member_name.present?

...

太过于繁琐!

2. 方案

于是考虑将中间的共性提取出来,做成一个单独的 concern。

直接上代码,刚写的,还热乎呢 😄 加了几个条件,更多的条件慢慢加上

#encoding: utf-8

module ActiveRecordSupport
  extend ActiveSupport::Concern

    def filter params={}
      result = self
      if respond_to?(:filter_mapping)
        filter_mapping.each do |field, config|

          if config.is_a?(Hash)
            method = config[:method]
            type = config[:type]
            default_value = config[:default]
          else
            method = config
            type = nil
          end

          value = params[field.to_sym]||default_value
          next if type.nil? && value.blank?

          case method.to_sym
            when :equal
              result = result.where("#{field.to_s}=?", value)
            when :like
              result = result.where("#{field.to_s} like ?", "%#{value}%")
            when :compare
              op = value[0]
              next unless %w{= > < >= <=}.include?(op)
              result = result.where("#{field.to_s} #{op} ?", "#{value[1..value.length]}")
            when :between
              value_begin = params["#{field.to_s}_begin"]
              value_end = params["#{field.to_s}_end"]

              if type == :date
                value_begin = value_begin.to_date.beginning_of_day if value_begin.present?
                value_end = value_end.to_date.end_of_day if value_end.present?
              elsif type == :time
                value_begin = value_begin.to_time.beginning_of_day if value_begin.present?
                value_end = value_end.to_time.end_of_day if value_end.present?
              end

              result = result.where("#{field.to_s} >= ?", value_begin) if value_begin.present?
              result = result.where("#{field.to_s} <= ?", value_end) if value_end.present?
            else
              # type code here
          end
        end
      end

      result
    end
  end

end

3 使用

3.1 等于和模糊查询

在 model 定义如下的元数据

class Account < ActiveRecord::Base
  include ActiveRecordSupport
  class<<self
    def filter_mapping
      {
        account: :equal,
        account_name: :like,
        status: :equal
      }
    end
  end
end
  • account 账号,等于查询
  • account_name 账号的中文名称,模糊 like 查询
  • status 账号状态,等于查询

这样在 Controller 中直接如下操作

class AccountsController < ConsoleController
  def index
    @accounts = Account.filter(params).page(@page)
  end

  ....
end

3.2 日期区间比较

class<<self
  def filter_mapping
    {
        account: :equal,
        day: {method: :between, type: :date}
    }
  end
end

  • day 统计日期到天

前段页面支持选择时间段,格式:字段_begin 和 字段_end

页面如下:

<div class="col-sm-2 m-b-xs">
  <input type="text" name="day_begin" placeholder="开始时间" class="input-sm datepicker">
</div>
<div class="col-sm-2 m-b-xs">
  <input type="text" name="day_end" placeholder="结束时间" class="input-sm datepicker">
</div>

  • 日期除了到天,还支持到秒
class<<self
  def filter_mapping
    {
      request_time: {method: :between,type: :time}
    }
  end
end

3.3 简单的数据比对

class<<self
  def filter_mapping
    {
      count: :compare,
    }
  end
end

注意前段需要将比较字符放在第一位,如:

<div class="col-sm-2 m-b-xs">
      <select name="count">
        <option value="">数量</option>
        <option value="=0">等于0</option>
        <option value=">0">大于0</option>
      </select>
    </div>

3.4 默认值配置

支持默认值,如查询默认是查询 大于 0 的内容

class<<self
  def filter_mapping
    {
        account: :equal,
        count: {method: :compare, default: '>0'},
    }
  end
end

想了想,主要用到也就这几个条件了,有再加,一会整理下放到 github。

@quakewang 不错的东西,因为比较简单的需求,就随手写了个 😄

我记得我们当时也写了个类似的 ext 方法出来 但是配合了前端 不会把为空的值传过来 就好解决多了

好大一坨函数代码,感觉好危险

@huacnlee 😍 嘘...,这个是目的,悄悄的在里面埋个那啥啥的的,嘿嘿 ...

还不如 1,一眼看出来什么意思。

一开始也是自己写,最后想肯定有人写类似的 gem 就找到了 https://github.com/activerecord-hackery/ransack

这么一大坨还不如定义几个 scope 清晰

%w[coach member training_item].each do |i|
  scope "by_#{i}_id".to_sym, ->(l){ where(i.to_sym => l) if l.present? }
end

更复杂了

这些做法纯粹是瞎折腾。用 scope 的话,名字起得不好都看不懂,更何况弄一堆约定似的 hash?

莫不如查查有没有 n+1,如果用 mysql,explain 一下,看看索引用的怎么样,这些貌似更实际点。

刚刚看了这个 Ransack,挺棒的推荐; 但是写API接口的时候,感觉还是还是最开始的拼凑查询条件更实用

2 是烂代码,1 才是好代码

martin 关闭了讨论。 02月06日 14:25
需要 登录 后方可回复, 如果你还没有账号请 注册新账号