Rails-构建动态查询以过滤搜索结果

时间:2016-12-05 17:22:00

标签: ruby-on-rails ruby activerecord arel

我正在尝试构建一个动态查询方法来过滤搜索结果。

我的模特:

class Order < ActiveRecord::Base
  scope :by_state, -> (state) { joins(:states).where("states.id = ?", state) }
  scope :by_counsel, -> (counsel) { where("counsel_id = ?", counsel) }
  scope :by_sales_rep, -> (sales) { where("sales_id = ?", sales) }
  scope :by_year, -> (year) { where("title_number LIKE ?", "%NYN#{year}%") }
  has_many :properties, :dependent => :destroy 
  has_many :documents, :dependent => :destroy
  has_many :participants, :dependent => :destroy
  has_many :states, through: :properties
  belongs_to :action
  belongs_to :role
  belongs_to :type
  belongs_to :sales, :class_name => 'Member'
  belongs_to :counsel, :class_name => 'Member'
  belongs_to :deal_name
end

class Property < ActiveRecord::Base
  belongs_to :order
  belongs_to :state
end

class State < ActiveRecord::Base
    has_many :properties
    has_many :orders, through: :properties
end

我有一个页面,我默认显示所有订单。我想要复选框以允许过滤结果。过滤器包括:年份,州,销售和律师。查询的一个示例是:2016年,2015年的所有订单(“order.title_number LIKE?”,“%NYN#{year}%”)处于州(has_many)NJ,PA,CA等州,具有sales_id无限ids和counsel_id无限的counsel_ids。

在一个坚果shell中,我试图弄清楚如何创建一个考虑用户检查的所有选项的查询。这是我当前的查询代码:

  def Order.query(opt = {})
    results = []
    orders = []

    if !opt["state"].empty?
      opt["state"].each do |value|
        if orders.empty? 
          orders = Order.send("by_state", value)  
        else
          orders << Order.send("by_state", value)
        end
      end
      orders = orders.flatten
    end

    if !opt["year"].empty?
      new_orders = []

      opt["year"].each do |y| 
        new_orders = orders.by_year(y)
        results << new_orders
      end
    end

    if !opt["sales_id"].empty?

    end

    if !opt["counsel_id"].empty?

    end
    if !results.empty?
      results.flatten 
    else
      orders.flatten
    end
  end

这是我提出的解决方案,允许无限量的过滤。

 def self.query(opts = {})

    orders = Order.all
    opts.delete_if { |key, value| value.blank? }
    const_query = ""
    state_query = nil
    counsel_query = nil
    sales_query = nil
    year_query = nil
    queries = []

    if opts["by_year"]  
      year_query = opts["by_year"].map do |val|
        " title_number LIKE '%NYN#{val}%' "
      end.join(" or ") 
      queries << year_query
    end     

    if opts["by_sales_rep"]
      sales_query = opts["by_sales_rep"].map do |val|
        " sales_id = '#{val}' "
      end.join(" or ")
      queries << sales_query
    end

    if opts["by_counsel"]
      counsel_query = opts["by_counsel"].map do |val|
        " counsel_id = '#{val}' "
      end.join(" or ")
      queries << counsel_query
    end

    if opts["by_state"]      
      state_query = opts["by_state"].map do |val|
        "states.id = '#{val}'"
      end.join(" or ")
    end

    query_string = queries.join(" AND ")

    if state_query
      @orders = Order.joins(:states).where("#{state_query}")
      @orders = @orders.where(query_string)
    else
      @orders = orders.where("#{query_string}")
    end

    @orders.order("title_number DESC")
  end

3 个答案:

答案 0 :(得分:2)

您正在寻找查询/过滤器对象,这是一种常见模式。我写了一篇类似于此的answer,但我会尝试提取重要部分。

首先,您应该将这些逻辑移到它自己的对象上。当搜索/过滤器对象被初始化时,它应该以关系查询(orders = OrderQuery.call(params)或一些基本查询)开始,然后在你去的时候过滤它。

这是一个超级基本的例子,没有充实,但应该让你走在正确的轨道上。您可以这样称呼# /app/services/order_query.rb class OrderQuery def call(opts) new(opts).results end private attr_reader :opts, :orders def new(opts={}) @opts = opts @orders = Order.all # If using Rails 3 you'll need to use something like # Order.where(1=1) to get a Relation instead of an Array. end def results if !opt['state'].empty? opt['state'].each do |state| @orders = orders.by_state(state) end end if !opt['year'].empty? opt['year'].each do |year| @orders = orders.by_year(year) end end # ... all filtering logic # you could also put this in private functions for each # type of filter you support. orders end end

# /app/services/order_query.rb
class OrderQuery
  def call(opts)
    new(opts).results
  end

  private

  attr_reader :opts, :orders

  def new(opts={})
    @opts = opts
    @orders = Order.all  # If using Rails 3 you'll need to use something like
                         # Order.where(1=1) to get a Relation instead of an Array.
  end

  def results
    if !opt['state'].empty?
      @orders = orders.where(state: opt['state'])
    end

    if !opt['year'].empty?
      @orders = orders.where(year: opt['year'])
    end

    # ... all filtering logic
    # you could also put this in private functions for each
    # type of filter you support.

    orders
  end
end

编辑:使用OR逻辑代替AND逻辑

if state is in this array of states

上述语法基本上会过滤语句year is within this array of yearsmark <- data[data[,1]=="Mark",] #so here you'll have all data for "Mark" tim <- data[data[,1]=="Tim",] #all data for "Tim" dave <- data[data[,1]=="Dave",] #all data for "Dave"

答案 1 :(得分:0)

在我的情况下,过滤器选项来自Controller的参数,所以我做了这样的事情:

ActionController::Parameters结构:

{
  all: <Can be true or false>,
  has_planned_tasks: <Can be true or false>
  ... future filters params
}

过滤方法:

  def self.filter(filter_params)
    filter_params.reduce(all) do |queries, filter_pair|
      filter_key = filter_pair[0]
      filter_value = filter_pair[1]

      return {
        all: proc { queries.where(deleted_at: nil) if filter_value == false },
        has_planned_tasks: proc { queries.joins(:planned_tasks).distinct if filter_value == true },
      }.fetch(filter_key).call || queries
    end
  end

然后我在控制器中调用ModelName.filter(filter_params.to_h)。这样就能轻松添加更多条件过滤器。

这里有改进的空间,例如提取过滤器逻辑或整个过滤器对象,但是我让您决定哪种方法在您的上下文中更好。

答案 2 :(得分:0)

这是我在 Rails 中为电子商务订单仪表板构建的,参数来自控制器。

这个查询会执行两次,一次是统计订单,一次是根据请求中的参数返回请求的订单。

此查询支持:

  • 按列排序
  • 排序方向
  • 增量搜索 - 它将搜索给定字段的开头并返回匹配的记录,从而在搜索时启用实时建议
  • 分页(每页限制为 100 条记录)

我也有预定义的值来清理一些数据。

这种风格非常干净,易于其他人阅读和修改。

这是一个示例查询:

api/shipping/orders?pageNumber=1&orderStatus=unprocessedOrders&filters=standard,second_day&stores=82891&sort_column=Date&sort_direction=dsc&search_query=916

这是控制器代码:

user_id = session_user.id
order_status = params[:orderStatus]

status = {
  "unprocessedOrders" => ["0", "1", "4", "5"],
  "processedOrders" => ["2", "3", "6"],
  "printedOrders" => ["3"],
  "ratedOrders" => ["1"],
}

services = [
  "standard",
  "expedited",
  "next_day",
  "second_day"
]

countries = [
  "domestic",
  "international"
]

country_defs = {
  domestic: ['US'],
  international: ['CA', 'AE', 'EU', 'GB', 'MX', 'FR']
}

columns = {
  Number: "order_number",
  QTY: "order_qty",
  Weight: "weight",
  Status: "order_status",
  Date: "order_date",
  Carrier: "ship_with_carrier",
  Service: "ship_with_carrier_code",
  Shipping: "requestedShippingService",
  Rate: "cheapest_rate",
  Domestic: "country",
  Batch: "print_batch_id",
  Skus: "skus"
}
# sort_column=${sortColumn}&sort_direction=${sortDirection}&search_query=${searchQuery}

filters = params[:filters].split(',')
stores = params[:stores].split(',')
sort_column = params[:sort_column]
sort_direction = params[:sort_direction]
search_query = params[:search_query]
sort_by_column = columns[params[:sort_column].to_sym]
sort_direction = params[:sort_direction] == "asc" ? "asc" : "desc"

service_params = filters.select{ |p| services.include?(p) }
country_params = filters.select{ |p| countries.include?(p) }
order_status_params = filters.select{ |p| status[p] != nil }

query_countries = []

query_countries << country_defs[:"#{country_params[0]}"]  if country_params[0]
query_countries << country_defs[:"#{country_params[1]}"] if country_params[1]

active_filters = [service_params, country_params].flatten

query = Order.where(user_id: user_id)
query = query.where(order_status: status[order_status]) if order_status_params.empty?
query = query.where("order_number ILIKE ? OR order_id::TEXT ILIKE ? OR order_info->'advancedOptions'->>'customField2' ILIKE ?", "%#{search_query}%", "%#{search_query}%", "%#{search_query}%") unless search_query.gsub(/\s+/, "").length == 0
query = query.where(requestedShippingService: service_params) unless service_params.empty?
query = query.where(country: "US") if country_params.include?("domestic") && !country_params.include?("international")
query = query.where.not(country: "US") if country_params.include?("international") && !country_params.include?("domestic")
query = query.where(order_status: status[order_status_params[0]]) unless order_status_params.empty?
query = query.where(store_id: stores) unless stores.empty?\

order_count = query.count
num_of_pages =  (order_count.to_f / 100).ceil()
requested_page = params[:pageNumber].to_i

formatted_number = (requested_page.to_s + "00").to_i

query = query.offset(formatted_number - 100) unless requested_page == 1
query = query.limit(100)
query = query.order("#{sort_by_column}": :"#{sort_direction}") unless sort_by_column == "skus"
query = query.order("skus[1] #{sort_direction}") if sort_by_column == "skus"
query = query.order(order_number: :"#{sort_direction}")
orders = query.all

puts "After querying orders mem:" + mem.mb.to_s

requested_page = requested_page <= num_of_pages ? requested_page : 1
options = {}
options[:meta] = {
  page_number: requested_page,
  pages: num_of_pages,
  type: order_status,
  count: order_count,
  active_filters: active_filters
}

render json: OrderSerializer.new(orders, options).serialized_json