Refactoring Model Methods in Ruby On Rails - ruby-on-rails

A common idiom that my camp uses in rails is as follows:
def right_things(all_things, value)
things = []
for thing in all_things
things << thing if thing.attribute == value
end
return things
end
how can I make this better/faster/stronger?
thx
-C

def right_things(all_things, value)
all_things.select{|x| x.attribute == value}
end

If your things are ActiveRecord models and you only need the items selected for your current purpose, you may, if you're using Rails 2.0 (? definitely 2.1) or above, find named_scopes useful.
class Thing
named_scope :rightness, lambda { |value| :conditions => ['attribute = ?', value] }
end
So you can say
Thing.rightness(123)
, which is (in this case) similar to
Thing.find_by_attribute(123)
in that it boils down to a SQL query, but it's more easily chainable to modify the SQL. If that's useful to you, which it may not be, of course...

Related

rails dynamic where sql query

I have an object with a bunch of attributes that represent searchable model attributes, and I would like to dynamically create an sql query using only the attributes that are set. I created the method below, but I believe it is susceptible to sql injection attacks. I did some research and read over the rails active record query interface guide, but it seems like the where condition always needs a statically defined string as the first parameter. I also tried to find a way to sanitize the sql string produced by my method, but it doesn't seem like there is a good way to do that either.
How can I do this better? Should I use a where condition or just somehow sanitize this sql string? Thanks.
def query_string
to_return = ""
self.instance_values.symbolize_keys.each do |attr_name, attr_value|
if defined?(attr_value) and !attr_value.blank?
to_return << "#{attr_name} LIKE '%#{attr_value}%' and "
end
end
to_return.chomp(" and ")
end
Your approach is a little off as you're trying to solve the wrong problem. You're trying to build a string to hand to ActiveRecord so that it can build a query when you should simply be trying to build a query.
When you say something like:
Model.where('a and b')
that's the same as saying:
Model.where('a').where('b')
and you can say:
Model.where('c like ?', pattern)
instead of:
Model.where("c like '#{pattern}'")
Combining those two ideas with your self.instance_values you could get something like:
def query
self.instance_values.select { |_, v| v.present? }.inject(YourModel) do |q, (name, value)|
q.where("#{name} like ?", "%#{value}%")
end
end
or even:
def query
empties = ->(_, v) { v.blank? }
add_to_query = ->(q, (n, v)) { q.where("#{n} like ?", "%#{v}%") }
instance_values.reject(&empties)
.inject(YourModel, &add_to_query)
end
Those assume that you've properly whitelisted all your instance variables. If you haven't then you should.

Rails find with a block

I have seen Rails find method taking a block as
Consumer.find do |c|
c.id == 3
end
Which is similar to Consumer.find(3).
What are some of the use cases where we can actually use block for a find ?
It's a shortcut for .to_a.find { ... }. Here's the method's source code:
def find(*args)
if block_given?
to_a.find(*args) { |*block_args| yield(*block_args) }
else
find_with_ids(*args)
end
end
If you pass a block, it calls .to_a (loading all records) and invokes Enumerable#find on the array.
In other words, it allows you to use Enumerable#find on a ActiveRecord::Relation. This can be useful if your condition can't be expressed or evaluated in SQL, e.g. querying serialized attributes:
Consumer.find { |c| c.preferences[:foo] == :bar }
To avoid confusion, I'd prefer the more explicit version, though:
Consumer.all.to_a.find { |c| c.preferences[:foo] == :bar }
The result may be similar, but the SQL query is not similar to Consumer.find(3)
It is fetching all the consumers and then filtering based on the block. I cant think of a use case where this might be useful
Here is a sample query in the console
consumer = Consumer.find {|c|c.id == 2}
# Consumer Load (0.3ms) SELECT `consumers`.* FROM `consumers`
# => #<Consumer id: 2, name: "xyz", ..>
A good example of a use-case is if you have a JSON/JSONB column and don't want to get involved in the more complex JSON SQL.
required_item = item_collection.find do |item|
item.jsondata['json_array_property'][index]['property'] == clause
end
This is useful if you can constrain the scope of the item_collection to a date-range, for example, and have a smaller set of items that require filtering further.

What's the best way to include a LIKE clause in a Rails query?

What's the best way to include a LIKE clause in a Rails query i.e. something along the lines of (the completely incorrect):
Question.where(:content => 'LIKE %farming%')
You'd use the syntax:
Question.where("content LIKE ?" , "%#{farming}%")
If this is Rails 3 you can use Arel's matches. This has the advantage of being database agnostic. For example:
Question.where(Question.arel_table[:content].matches("%#{string}%"))
This is somewhat clunky, but easily extracted to scopes, e.g.:
class Question
def self.match_scope_condition(col, query)
arel_table[col].matches("%#{query}%")
end
scope :matching, lambda {|*args|
col, opts = args.shift, args.extract_options!
op = opts[:operator] || :or
where args.flatten.map {|query| match_scope_condition(col, query) }.inject(&op)
}
scope :matching_content, lambda {|*query|
matching(:content, *query)
}
end
Question.matching_content('farming', 'dancing') # farming or dancing
Question.matching_content('farming', 'dancing', :operator => :and) # farming and dancing
Question.matching(:other_column, 'farming', 'dancing') # same thing for a different col
Of course to join with "AND" you could just chain the scopes.
Edit: +1 to metawhere and squeel though (haven't tried latter but it looks cool) They both add this type of functionality and much more.
If you want really sexy conditions and and don't have problems with external dependencies, I highly recommend MetaWhere and it's successor Squeel:
# MetaWhere
Question.where(:content.like => '%farming%')
# MetaWhere with operators overloaded
Question.where(:content =~ '%farming%')
# Squeel
Question.where { :content.matches => '%farming%' }
# Squeel with operators overloaded
Question.where { :content =~ '%farming%' }

Reusing named_scope to define another named_scope

The problem essence as I see it
One day, if I'm not mistaken, I have seen an example of reusing a named_scope to define another named_scope. Something like this (can't remember the exact syntax, but that's exactly my question):
named_scope :billable, :conditions => ...
named_scope :billable_by_tom, :conditions => {
:billable => true,
:user => User.find_by_name('Tom')
}
The question is: what is the exact syntax, if it's possible at all? I can't find it back, and Google was of no help either.
Some explanations
Why I actually want it, is that I'm using Searchlogic to define a complex search, which can result in an expression like this:
Card.user_group_managers_salary_greater_than(100)
But it's too long to be put everywhere. Because, as far as I know, Searchlogic simply defines named_scopes on the fly, I would like to set a named_scope on the Card class like this:
named_scope from_big_guys, { user_group_managers_salary_greater_than(100) }
- this is where I would use that long Searchlogic method inside my named_scope. But, again, what would be the syntax? Can't figure it out.
Resume
So, is named_scope nesting (and I do not mean chaining) actually possible?
You can use proxy_options to recycle one named_scope into another:
class Thing
#...
named_scope :billable_by, lambda{|user| {:conditions => {:billable_id => user.id } } }
named_scope :billable_by_tom, lambda{ self.billable_by(User.find_by_name('Tom').id).proxy_options }
#...
end
This way it can be chained with other named_scopes.
I use this in my code and it works perfectly.
I hope it helps.
Refer to this question raised time ago here at SO.
There is a patch at lighthouse to achieve your requirement.
Rails 3+
I had this same question and the good news is that over the last five years the Rails core team has made some good strides in the scopes department.
In Rails 3+ you can now do this, as you'd expect:
scope :billable, where( due: true )
scope :billable_by_tom, -> { billable.where( user: User.find_by_name('Tom') ) }
Invoice.billable.to_sql #=> "... WHERE due = 1 ..."
Invoice.billiable_by_tom.to_sql #=> "... WHERE due = 1 AND user_id = 5 ..."
FYI, Rails 3+ they've renamed named_scope to just scope. I'm also using Ruby 1.9 syntax.
Bonus Round: Generic Scope.
If there are multiple people that are "billable" besides just "Tom" then it might be useful to make a generic scope that accepts a name param that gets passed into the block:
scope :billable_by, lambda { |name| billable.where( user: User.find_by_name( name ) ) }
Then you can just call it with:
Invoice.billable_by( "Tom" ).to_sql #=> "... WHERE due = 1 AND user_id = 5 ..."
Btw, you can see I used the older lambda syntax in the bonus round. That's because I think the new syntax looks atrocious when you're passing a param to it: ->( name ) { ... }.
Chain Scopes.
Why not have a scope for stuff just by Tom in general, like:
scope :by_tom, where( user: User.find_by_name('Tom') )
And then you can get those records that are "billable by Tom" with:
Record.billable.by_tom
You can use a method to combine some named_scope like :
def self.from_big_guys
self.class.user_group_managers_salary_greater_than(100)
end
This feature is add on Rails 3 with new syntax (http://m.onkey.org/2010/1/22/active-record-query-interface)

How to apply named_scopes incrementally in Rails

named_scope :with_country, lambad { |country_id| ...}
named_scope :with_language, lambad { |language_id| ...}
named_scope :with_gender, lambad { |gender_id| ...}
if params[:country_id]
Event.with_country(params[:country_id])
elsif params[:langauge_id]
Event.with_state(params[:language_id])
else
......
#so many combinations
end
If I get both country and language then I need to apply both of them. In my real application I have 8 different named_scopes that could be applied depending on the case. How to apply named_scopes incrementally or hold on to named_scopes somewhere and then later apply in one shot.
I tried holding on to values like this
tmp = Event.with_country(1)
but that fires the sql instantly.
I guess I can write something like
if !params[:country_id].blank? && !params[:language_id].blank? && !params[:gender_id].blank?
Event.with_country(params[:country_id]).with_language(..).with_gender
elsif country && language
elsif country && gender
elsif country && gender
.. you see the problem
Actually, the SQL does not fire instantly. Though I haven't bothered to look up how Rails pulls off this magic (though now I'm curious), the query isn't fired until you actually inspect the result set's contents.
So if you run the following in the console:
wc = Event.with_country(Country.first.id);nil # line returns nil, so wc remains uninspected
wc.with_state(State.first.id)
you'll note that no Event query is fired for the first line, whereas one large Event query is fired for the second. As such, you can safely store Event.with_country(params[:country_id]) as a variable and add more scopes to it later, since the query will only be fired at the end.
To confirm that this is true, try the approach I'm describing, and check your server logs to confirm that only one query is being fired on the page itself for events.
Check Anonymous Scopes.
I had to do something similar, having many filters applied in a view. What I did was create named_scopes with conditions:
named_scope :with_filter, lambda{|filter| { :conditions => {:field => filter}} unless filter.blank?}
In the same class there is a method which receives the params from the action and returns the filtered records:
def self.filter(params)
ClassObject
.with_filter(params[:filter1])
.with_filter2(params[:filter2])
end
Like that you can add all the filters using named_scopes and they are used depending on the params that are sent.
I took the idea from here: http://www.idolhands.com/ruby-on-rails/guides-tips-and-tutorials/add-filters-to-views-using-named-scopes-in-rails
Event.with_country(params[:country_id]).with_state(params[:language_id])
will work and won't fire the SQL until the end (if you try it in the console, it'll happen right away because the console will call to_s on the results. IRL the SQL won't fire until the end).
I suspect you also need to be sure each named_scope tests the existence of what is passed in:
named_scope :with_country, lambda { |country_id| country_id.nil? ? {} : {:conditions=>...} }
This will be easy with Rails 3:
products = Product.where("price = 100").limit(5) # No query executed yet
products = products.order("created_at DESC") # Adding to the query, still no execution
products.each { |product| puts product.price } # That's when the SQL query is actually fired
class Product < ActiveRecord::Base
named_scope :pricey, where("price > 100")
named_scope :latest, order("created_at DESC").limit(10)
end
The short answer is to simply shift the scope as required, narrowing it down depending on what parameters are present:
scope = Example
# Only apply to parameters that are present and not empty
if (!params[:foo].blank?)
scope = scope.with_foo(params[:foo])
end
if (!params[:bar].blank?)
scope = scope.with_bar(params[:bar])
end
results = scope.all
A better approach would be to use something like Searchlogic (http://github.com/binarylogic/searchlogic) which encapsulates all of this for you.

Resources