Rails- 构建用于过滤搜索结果的动态查询

Rails- Building dynamic query for filtering search results

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

我的模特:

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 到)新泽西州、宾夕法尼亚州、加利福尼亚州等 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

你要找的是一个query/filter对象,这是一个常见的模式。我写了一个与此类似的answer,但我会尝试提取重要部分。

首先你应该将这些逻辑移动到它自己的对象中。当 search/filter 对象被初始化时,它应该从一个关系查询(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.

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

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

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

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

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

该查询支持:

  • 按列排序
  • 排序方向
  • 增量搜索 - 它将搜索给定字段的开头,并 returns 那些匹配的记录在搜索时启用实时建议
  • 分页(每页限制 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