Managing multiple GET parameters in Rails 5 API

2019-09-12 10:46发布

问题:

I'm using Rails 5 API to build an app for retrieving data. I want to use multiple GET parameters to extract data.

Sample GET Request Format

/users?company=Samsung&position=Engineer

Controller Code for single parameter:-

def index
client = User.where("company = ?", params[:company])
render json: client
end

Controller Code for multiple parameters:-

def index
client = User.where("company = ?", params[:company]).where("position = ?", params[:position])
render json: client
end

Likewise there are many parameters which may/may not be included in the GET request.

What I want

If a user hasn't specified any parameter, he should be shown all the users. If only a few parameters have been specified, only those parameters should be searched.

What is happening

If a user specifies only position parameter in the GET request, Rails can't handle it & shows a blank json as there's no rule specified for it.

Please note that there are many parameters and it's not possible to write a rule for each and every combination of parameters.

回答1:

The basic issue with your solution is that it's forcing each where to execute in turn. This means that if the company parameter is nil you get an empty relation and all where statements after that will return an empty relation. You need to conditionally filter only if the parameters are provided.

What you're trying to do is best done inside a query/search or filter object (query takes parameters and returns a result set, filter takes a set already generated and filters it down). I like to define these in app/services/ although some people put them in app/queries.

There are some inconsistencies in your method however, an index endpoint should return a list of objects. Even if you're hoping to get back a single option you should expect that there could be multiple. Let's say we fix up your index endpoint and use a filter object.

# app/controllers/users_controller.rb
def index
  users = User.where(1=1)
  users = Users::Filter.call(users, params)
  render json: users
end

You'll notice I use User.where(1=1), this is because I'm use to dealing with rails 3 which returns an array instead of a relation if you use User.all.

# /app/services/users/filter.rb
class Users::Filter
  def self.call(resources, options)
    new(users, options).filter
  end

  private

  attr_reader :resources, :options

  def initialize(resources, options)
    @resources = resources
    @options   = options
  end

  def filter
    if options[:company]
      @resources = resources.where(company: options[:company])
    end

    if options[:position]
      @resources = resources.where(position: options[:position])
    end

    resources
  end
end

The reason I use call which then invokes the private initialize and filter method is so that this class isn't used incorrectly. It has a single point of entry and returns the result set. This also allows me to break out my filtering functions into small bite size chunks that are easy to change.

For this example I did all of the filtering in filter but you could break these out into methods and even use some clever meta programming to call the filter name if the object responds to it.

EDIT:

You may also consider finding a gem solution like ransack[1] which allows you to pass parameters directly to the object and search it. There are many different types of search/filtering gems out there to meet your needs.

[1] https://github.com/activerecord-hackery/ransack