6

My question is similar to this one, but none of the answers there address my specific issue.

I want to find objects with something like this:

conditions = {first_name: @search OR last_name: @search}
Stuff.where( conditions )

Obviously, this syntax is invalid, but it's the easiest way I can think of to show what I want to do. I want to use the cleaner hash syntax with complex/compound conditions.

I know you can just write it out by hand with "pure string conditions" like this Stuff.where("first_name=#{@search} OR last_name=#{@search}") ...but this is NOT what I want to know.

Update It looks like you can do OR with an array like this: Stuff.where( some_column: ["option1", "option2"]). This is quite useful to know, but it doesn't solve my problem because I need the OR to apply to different key=value pairs... key=value OR key=value not key=value OR value.

Update2 The reason that I don't want to user the SQL string is because I need to build the query in several pieces and I don't know how to do that while still escaping the inserted variables. I haven't tested, but I'm assuming this won't work:

conditions = ["(project_id=? AND sent_to_api=1)", @project_id]
conditions = conditions + ["first_name=? OR last_name=?", @search, @search]
Stuff.where( conditions )

Hopefully this makes sense. Is there a way to do what I need with the SQL string syntax while still preserving Rails's built-in SQL escaping?

Community
  • 1
  • 1
emersonthis
  • 32,822
  • 59
  • 210
  • 375
  • 4
    If the **only** way to do it is to write out the conditions as a string like `Stuff.where(["first_name = ? OR last_name = ?", @search])` do you want to know that? – Shadwell Sep 10 '13 at 15:31
  • possible duplicate of [ActiveRecord OR query](http://stackoverflow.com/questions/3639656/activerecord-or-query) – fotanus Sep 10 '13 at 15:33
  • That's pretty well documented, so thanks but no. (I know my straw man example isn't optimal) – emersonthis Sep 10 '13 at 15:34
  • Have a look at the [Squeel gem](https://github.com/ernie/squeel) for cleaner ActiveRecord syntax. – Mischa Sep 10 '13 at 15:40
  • 1
    The updated method doesn't actually generate `OR`, it uses `IN ()` ...which is generally interpreted as `OR` by the SQL engine, but I wanted to point this out for clarification. – PinnyM Sep 10 '13 at 15:45
  • RE: update2 - yes, the method I wrote does escaping for you... – PinnyM Sep 10 '13 at 15:51

5 Answers5

6

How about a reusable method that generates the dreaded SQL strings for you, and safely at that:

class ActiveRecord::Base
  def self.in_any(value, *column_names)
    raise 'At least one column must be supplied' unless column_names.present?
    sql_fragment = column_names.map{|f| "#{quote_column_name(f)} = :search"}.join(" OR ")
    where(sql_fragment, search: value)
  end
end

Stuff.in_any(@search, :first_name, :last_name)

UPDATE

If you don't want to add a method for some reason, you can do this on the fly quite safely without fear of injection. The most elegant way in this case (IMHO) would be to chain the filters together:

Stuff.where('project_id = ? AND sent_to_api = 1', @project_id).
      where('first_name = :search OR last_name = :search', search: @search)
PinnyM
  • 35,165
  • 3
  • 73
  • 81
  • I didn't know you could chain more than one .where on a single query. That opens up a few possibilities... Can you think of a way to conditionally tack on .where methods depending on an external factor? `Stuff.where(project_id: id).where( something ? ['first_name=?', @search] : nil )` – emersonthis Sep 10 '13 at 16:20
  • @Emerson - that code should work, although you might need to replace `nil` with an empty array (not sure about this). Even simpler: `where(['first_name=?', @search] if something)`. – PinnyM Sep 10 '13 at 16:25
  • It seems to work! But I'm having trouble getting the @search inserted properly inside a LIKE: `...LIKE %?%", @search` produces `last_name LIKE %'foobar'%` Note the single quotes inside the `%` – emersonthis Sep 10 '13 at 16:31
  • @Emerson, right - instead of `LIKE %?` do this: `where('foo LIKE ?', "%#{@search}%")`. This will cause the '%' to be escaped along with the search string. – PinnyM Sep 10 '13 at 16:33
  • Will @search still get escaped by Rails? – emersonthis Sep 10 '13 at 16:34
  • @Emerson, absolutely. Anything that you don't inject directly into the first element (the query string template) will be escaped as needed. – PinnyM Sep 10 '13 at 16:35
4

Rails 5 Syntax is

Stuff.where(first_name: @search).or(Stuff.where(last_name: @search))

For older versions of Rails, you can use sql string

Stuff.where(["first_name = ? or last_name = ?", 'John', 'Smith'])

Or if both query parameters are same you can also use a Hash

Stuff.where(["first_name = :name or last_name = :name", name: @search])
Santhosh
  • 28,097
  • 9
  • 82
  • 87
1

You have search fields array like

search_fields = ['first_name', 'last_name', 'email']

params comes from controllers

params = {'first_name' => 'abc', 'email' => abc@gmail.com}

search_field_params = params.slice(*search_fields)

query = search_field_params.keys.map{|field| "#{field} = :#{field}" }.join(" OR ")

query_params = search_field_params.symbolize_keys

Stuff.where(user_id: user_id).where(query, query_params)

SELECT * FROM stuffs WHERE user_id = 537 AND (first_name = 'abc' OR email = 'abc@gmail.com')

more conditions add with where..

Ravindra
  • 130
  • 11
0

I can only think of three ways to do this:

  1. Write SQL strings (.where('first_name = ? OR last_name = ?', 'foo', 'bar'). You've said you don't want to do this. Fair enough.

  2. Use AREL. The question linked by fotanus covers this: ActiveRecord OR query. Your comment suggests you don't like this option either.

  3. Use Squeel or some other gem that provides its own DSL for query creation. This should let you write your query as:

    Stuff.where{(first_name = search) | (last_name = search)}

Update: Off the top of my head, here's a way to build the query up in a few steps if you're on board with writing SQL strings. This feels a bit old - there's probably more elegant ways, but it should work fine:

conditions = []
values = []

conditions << 'first_name = ?'
values << 'Bob'

conditions << 'last_name = ?'
values << 'Smith'

#repeat as needed.

Stuff.where(conditions.join(' OR '), values)
Community
  • 1
  • 1
MrTheWalrus
  • 9,670
  • 2
  • 42
  • 66
-1

For rails 5 it should be available to write condition with #or method:

Stuff.where( first_name: @search ).or( Stuff.where( last_name: @search ) )

But for older versions Arel technique is available:

condition = Stuff.arel_table[:first_name].eq( @search ).or( Stuff.arel_table[:last_name].eq( @search ) )
Stuff.where( condition )
Малъ Скрылевъ
  • 16,187
  • 5
  • 56
  • 69