【问题标题】:Rails- Building dynamic query for filtering search resultsRails - 构建用于过滤搜索结果的动态查询
【发布时间】:2016-12-05 17:21:59
【问题描述】:

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

我的模型:

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}%”)在 NJ、PA、CA 等州(has_many through)NJ、PA、CA 等,sales_id 无限制 id 和Counsel_id 无限制的 Counsel_id。

简而言之,我试图弄清楚如何创建一个考虑到用户检查的所有选项的查询。这是我当前的查询代码:

  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

【问题讨论】:

  • 您的意思是您需要一种方法来调用所有这些查询吗?因为在您的代码中您调用 by_state,它是一个,然后我假设 year 是另一个,依此类推。所以这不是一个查询。在那种情况下(如果是 4 个查询)我真的看不出有什么问题......
  • 是的,我想用一个查询来调用所有内容。目前我正在尝试根据传递给查询方法的参数单独调用查询。参数的示例是 {"year"=>["16", "15"], "state"=>["31", "39"]}。我认为我的问题是能够在 _by_state 条件下多次调用 _by_year 。但不知多少年,州会发。我希望这是有道理的@Joel_Blum
  • 为什么不尝试按照scopes 命名 opt_keys,例如{by_state: ["31","39"],by_year: "16"} 那么你通常可以使用类似opts.inject(Order) {|order,(scope,value)| order.public_send(scope,value)} 的东西。这将构建一个包含所有指定过滤器的 ActiveRecord::Relation。然后,当您对其采取行动时,查询实际上会触发。顺便说一句,如果您希望通过多年,您将需要修复 by_year 范围。
  • 您可以使用在最近的 ruby​​ 版本 (> 1.9) 中接受可选参数的范围方法。你用的是什么红宝石?

标签: ruby-on-rails ruby activerecord arel


【解决方案1】:

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

首先,您应该将这些逻辑移至它自己的对象。初始化搜索/过滤器对象时,它应该以关系查询(Order.all 或一些基本查询)开始,然后随时过滤。

这是一个超级基本的例子,它没有充实,但应该能让你走上正确的道路。你可以这样称呼它,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

编辑:使用 OR 逻辑而不是 AND 逻辑

# /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

上面的语法基本上过滤了if state is in this array of statesyear is within this array of years的说法。

【讨论】:

  • 谢谢!,我遵循逻辑并已开始实施。我现在主要关心的是如果我想同时应用多个过滤器怎么办。例如,4 个州和 3 年。我怎样才能通过@orders 并明确地说,我需要这 4 个州内这 3 年的订单?我目前看到您的解决方案只为每个类别选择一个选项。再次感谢您的帮助。
  • 听起来你想要的是 OR 逻辑而不是 AND 逻辑。上面的代码使用 AND 类型语法迭代过滤。如果您希望它是 OR 逻辑(即,2015 年 OR 2016 年的订单),您需要稍微修改逻辑。上面更新了代码。
  • 我正在寻找 AND 和 OR 逻辑。不同过滤选项(州、年份等)的 AND 逻辑。当有多个过滤选项时的 OR 逻辑。例子。销售员 1、2、3 于 2016 年、2015 年在纽约、新泽西、宾夕法尼亚、马萨诸塞州的所有订单。
  • 好吧,我已经为您提供了两种类型的过滤逻辑。是否还有问题没有得到解答?
  • 您好!我根据您的一些建议提出了解决方案。查看有问题的更新代码。
【解决方案2】:

在我的例子中,过滤器选项来自控制器的参数,所以我做了这样的事情:

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)。我可以像这样轻松添加更多条件过滤器。

这里有改进的空间,比如提取过滤器逻辑或整个过滤器对象,但我让你决定在你的上下文中什么更好。

【讨论】:

    【解决方案3】:

    这是我在 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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-01-05
      • 1970-01-01
      • 2014-06-29
      • 1970-01-01
      • 1970-01-01
      • 2023-03-14
      相关资源
      最近更新 更多