0

For example in my Car model i have such fields:

color, price, year

and in form partial i generate form with all this fields. But how to code such logic:

user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...

And how to write where condition? I could write something like:

if params[:color].present?
car = Car.where(color: params[:color])
end

if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....

But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?

brabertaser19
  • 5,678
  • 16
  • 78
  • 184

5 Answers5

4

Check out the has_scope gem: https://github.com/plataformatec/has_scope

It really simplifies a lot of this:

class Graduation < ActiveRecord::Base
  scope :featured, -> { where(:featured => true) }
  scope :by_degree, -> degree { where(:degree => degree) }
  scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end

class GraduationsController < ApplicationController
  has_scope :featured, :type => :boolean
  has_scope :by_degree
  has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash

  def index
    @graduations = apply_scopes(Graduation).all
  end
end

Thats it from the controller side

John Naegle
  • 8,077
  • 3
  • 38
  • 47
2

I would turn those into scopes on your Car model:

scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}

and in your controller you would just conditionally chain them like this:

def index
  @cars = Car.all

  @cars = @cars.by_color(params[:color]) if params[:color].present?
  @cars = @cars.by_year(params[:year]) if params[:year].present?
end
tirdadc
  • 4,603
  • 3
  • 38
  • 45
  • Whoever upvoted - the question refers to multiple params -- populating `@cars` means only one set of data can be returned – Richard Peck Mar 26 '14 at 14:45
  • Right, I'm assuming you want the intersection of the data set if multiple params are set, ie all 1989 cars that are red. – tirdadc Mar 26 '14 at 14:46
  • It's not my Q, just saying - `user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...` - your answer, although really good, only allows one dataset to be returned – Richard Peck Mar 26 '14 at 14:47
  • @RichPeck upvote is mine. And this code works exactly as OP expects. – Marek Lipka Mar 26 '14 at 14:49
  • @RichPeck because of...? – Marek Lipka Mar 26 '14 at 14:51
  • Because you're pulling all data & then splicing it up. Maybe it's because I'm not experienced enough to know that `.all` doesn't pull the data until it's outputted or something, but I believe this needs its own `class method` which triggers `where` with only the submitted params – Richard Peck Mar 26 '14 at 14:54
  • 1
    @RichPeck You answered yourself. You're not experienced enough to know that the relations are evaluated lazily, only when their value is needed. That's when SQL query is generated. So this piece of code will generate only one SQL query. – Marek Lipka Mar 26 '14 at 14:55
  • Appreciate the feedback, but I still believe my idea is right – Richard Peck Mar 26 '14 at 14:57
  • This works fine, somewhat verbose though, why not just use strong parameters then it's one line and you use the where block like normal? (N.B. I would normally create scopes like this if I needed the functionality for anything else, just think it's unneccessary for OPs question. – Mike H-R Mar 26 '14 at 14:58
1
user_params = [:color, :year, :price]

cars = self
user_params.each do |p|
    cars = cars.where(p: params[p]) if params[p].present?
end
Raj
  • 22,346
  • 14
  • 99
  • 142
  • also i use rails4; and how to be if i need area, like: price between params[:price1] and params[:price2] how to do like: where params[:price1] <= price <= params[:price2] – brabertaser19 Mar 26 '14 at 14:42
0

The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.

class Car < ActiveRecord::Base
  # Just pass params directly in
  def self.search(params)
    # By default we return all cars
    cars = all

    if params[:color].present?
      cars = cars.where(color: params[:color])
    end

    if params[:price1].present? && params[:price2].present?
      cars = cars.where('price between ? and ?', params[:price1], params[:price2])
    end

    # insert more fields here

    cars
  end
end

You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).

sevenseacat
  • 24,699
  • 6
  • 63
  • 88
-1

I think you could use params.permit

my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)

EDIT: I think this only works in rails 4, not sure what version you're using.

EDIT #2 excerpt from site I linked to:

Using permit won't mind if the permitted attribute is missing

params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}

as you can see, foobar isn't inside the new hash.

EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.

Mike H-R
  • 7,726
  • 5
  • 43
  • 65
  • This IMO should be the chosen answer – Richard Peck Mar 26 '14 at 15:09
  • 1
    thanks @RichPeck. All the solutions seem to work fine, I just think this is simplest, glad someone agrees. :) – Mike H-R Mar 26 '14 at 15:12
  • this won't work fine - if the user doesn't specify a color to search for (for example) it'll only return cars that don't have a color. – sevenseacat Mar 27 '14 at 01:21
  • @sevenseacat, no it won't, params.permit creates a hash that only has the things you put in it, so if, for example, we only search for price, so the only one of those variables in the params that is present is price, then `my_where_params = {price: params[:price]}` so that is equivalent to a search for `car = Car.where(price: params[:price])` and that will include cars of all colors. See [the article I linked to](http://blog.sensible.io/2013/08/17/strong-parameters-by-example.html), or try evaluating params.permit in this case to see what happens. Let me know if that makes sense. – Mike H-R Mar 27 '14 at 11:22
  • how to be with params with <= => ? like price between .. ? in form i send price_from and price_to, how to compare it with your code? also i use rails4 – brabertaser19 Mar 27 '14 at 21:08
  • 1
    The user is submitting a form which will have fields for price, color and year (among other things, guessing). So the fields will always be present, but may not be filled out. If the user doesn't want to search on price, the price field will still be submitted, just empty - this will send the empty price field to the model and completely wreck your searching. – sevenseacat Mar 28 '14 at 01:34
  • I suppose that's a good point @sevenseacat I hadn't considered that (I was thinking about check-boxes which [don't activate the params hash](http://stackoverflow.com/questions/17354053/rails-4-strong-parameters-handling-missing-model-params-hash) when not selected). I'll edit my solution to work now. – Mike H-R Mar 28 '14 at 15:37
  • @brabertaser1992 for that case I would use scopes (as described in @tirdadc's answer) or just inline them with an extra where afterwards. e.g. for price between `.where('price < ?', price_to).where('price > ?', price_from)` after the where block described above. – Mike H-R Mar 28 '14 at 15:46