I am new to rails. Here is the following code with Foo as model object:
a = Foo
a = Foo.where(age: 18)
if params[:sort] == "desc"
a = a.order("name desc")
end
Here two queries are performed, I want to combine them to one or you can say i want to perform Foo.where(age=18).order("name asc")
Remember there can be the case when order is no needed i.e. params[:sort] is not equal to desc.
Please don't give solution like
if params[:sort] == "desc"
a = a.where(age=18).order("name desc")
else
a = a.where(age=18)
end
as it makes code redundant and also for more parameters it might not work.
No, you're mistaken. Actually, no queries are performed here.
a = Foo
a = Foo.where(age=18)
if params[:sort] == "desc"
a = a.order("name desc")
end
The actual query is sent where you start retrieving data. That is, do something like
a.each do |b|
# do something with b
end
Until then you can safely chain criteria building methods (where, order, select and others).
Actually your code will only execute one query. this is because in rails the calls to the database are only done once you access the result. So when you will write a.first (or something similar) it will make the DB call.
If its that what you mean... A simple solution would be:
a.where(age: 18).order("name #{params[:sort] || 'asc'}")
So if params[:sort] is nil it will default to asc.
Related
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.
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.
So, I'm pretty much a newbie at creating ruby methods. I've got the following method from a gem (meta_search), but I need to change the default behavior. Here's the method:
def sort_link(builder, attribute, *args)
raise ArgumentError, "Need a MetaSearch::Builder search object as first param!" unless builder.is_a?(MetaSearch::Builder)
attr_name = attribute.to_s
name = (args.size > 0 && !args.first.is_a?(Hash)) ? args.shift.to_s : builder.base.human_attribute_name(attr_name)
prev_attr, prev_order = builder.search_attributes['meta_sort'].to_s.split('.')
current_order = prev_attr == attr_name ? prev_order : nil
new_order = current_order == 'asc' ? 'desc' : 'asc'
options = args.first.is_a?(Hash) ? args.shift : {}
html_options = args.first.is_a?(Hash) ? args.shift : {}
css = ['sort_link', current_order].compact.join(' ')
html_options[:class] = [css, html_options[:class]].compact.join(' ')
options.merge!(
builder.search_key => builder.search_attributes.merge(
'meta_sort' => [attr_name, new_order].join('.')
)
)
link_to [ERB::Util.h(name), order_indicator_for(current_order)].compact.join(' ').html_safe,
url_for(options),
html_options
end
This method returns a sort link for a search. The output looks like this:
<a class="sort_link asc" href="/photos?search[meta_sort]=average_rating.desc">Best Photography ▲</a>
Here's the problem: this method assumes that you want to sort in ascending order on the first click, and descending order on the second. I want the opposite behavior. I see that I could change this...
new_order = current_order == 'asc' ? 'desc' : 'asc'
to this...
new_order = current_order == 'desc' ? 'asc' : 'desc'
...but that just reverses the situation. What I really need is to be able to specify an option and reverse the behavior if that option is passed.
So here's my problem: I don't really understand how the *args are passed. From what I can tell these lines are taking the option and html_option hashes for rails link_to method...
options = args.first.is_a?(Hash) ? args.shift : {}
html_options = args.first.is_a?(Hash) ? args.shift : {}
What I'd like to do is add a custom option to the options hash, and if that option is defined reverse the sort equation. I tried to do this...
if defined? options[:sort_preference] && options[:sort_preference]==:desc
new_order = current_order == 'desc' ? 'asc' : 'desc'
options.delete(:sort_preference)
else
new_order = current_order == 'asc' ? 'desc' : 'asc'
options.delete(:sort_preference)
end
... but that didn't work, it just kept the standard behavior and passed :sort_preference into the URL like so:
<a class="sort_link asc" href="/photos?search[meta_sort]=average_rating.desc&sort_preference=desc">Best Photography ▲</a>
In the link above, sort_preference shouldn't appear in the URL, and search[meta_sort] should be average_rating.asc -- since the link shows the opposite of what rendered.
So, obviously, my attempt didn't work because I don't really understand what's going on in this method. What I'm hoping for in answer to this question is:
A little help understanding how *args works
An explanation of why my attempt to add an option to this method failed
An example of how to fix it
Thank you very much for taking a look!
*args just means that you can pass unlimited number of arguments. So this method needs 2 arguments first (builder & attribute) and then you could pass any number of args after that. Here is a description of what is going on in the method relating to the args:
If the third argument passed to this method is not a hash, then it sets that arg as the name and removes it (args.shift), otherwise use the builder name.
Then it looks at the next arg (which would be the 4th). If its a hash, then it sets options to that arg and deletes that arg, otherwise it sets it to empty hash.
Then it looks at the next arg (5th arg). If its a hash, then it sets that as html_options and deletes that arg, otherwise html is set to {}.
Using args* can make calling the method a little cleaner (although the implementation in the method might not be). If you did not have a "name" to pass, instead of doing this:
sort_link(some_builder,some_attribute,nil,options)
you can do this:
sort_link(some_builder,some_attribute,options)
It does not force you to add a arg for name, if you don't have one.
So for your code to work, this method would need to take options as the 4th arg, and the options hash would need to contain :sort_preferences. What does the code look like that is calling sort_link? You need to insert this option into that hash before it gets to this method.
this method assumes that you want to sort in ascending order on the first click, and descending order on the second. I want the opposite behavior.
The meta_search gem supports this. It is not necessary to modify the gem's code. In your controller, after you have queried for your data, set the meta_sort attribute on the #search object to the field and sort order you would like. When the view loads, your data will be sorted as expected.
For example, in a Posts controller, you would do the following to set the default sort to the title field, descending:
def index
#search = Post.search(params[:search])
#search.meta_sort ||= 'title.desc'
#posts= #search.all
end
Given a query like:
current_user.conversations.where("params[:projectid] = ?", projectid).limit(10).find(:all)
params[:projectid] is being sent from jQuery ajax. Sometimes that is an integer and the above works fine. But if the use selects "All Projects, that's a value of '' which rails turns into 0. which yields an invalid query
How with rails do you say search params[:projectid] = ? if defined?
Thanks
I think you may have mistyped the query a bit. "params[:projectid] = ?" shouldn't be a valid query condition under any circumstances.
In any case, you could do some sort of conditional statement:
if params[:project_id].blank?
#conversations = current_user.conversations.limit(10)
else
#conversations = current_user.conversations.where("project_id = ?", params[:project_id]).limit(10)
end
Although, I'd probably prefer something like this:
#conversations = current_user.conversations.limit(10)
#converstaions.where("project_id = ?", params[:project_id]) unless params[:project_id].blank?
Sidenotes:
You don't have to use .find(:all). Rails will automatically execute the query when the resultset is required (such as when you do #conversations.each).
Wherever possible, try to adhere to Rails' snakecasing naming scheme (eg. project_id as opposed to projectid). You'll save yourself and collaborators a lot of headaches in the long run.
Thanks but if the where query has lets say 3 params, project_id, project_status, ... for example, then the unless idea won't work. I'm shocked that Rails doesn't have a better way to handle conditional query params
EDIT: If you have multiple params that could be a part of the query, consider the fact that where takes a hash as its argument. With that, you can easily build a parameter hash dynamically, and pass it to where. Something like this, maybe:
conditions = [:project_id, :project_status, :something_else].inject({}) do |hsh, field|
hsh[field] = params[field] unless params[field].blank?
hsh
end
#conversations = current_user.conversations.where(conditions).limit(10)
In the above case, you'd loop over all fields in the array, and add each one of them to the resulting hash unless it's blank. Then, you pass the hash to the where function, and everything's fine and dandy.
I didn't understand why you put:
where("params[:projectid] = ?", projectid)
if you receive params[:project] from the ajax request, the query string shouldn't be:
where("projectid = ?", params[:projectid])
intead?
And if you are receiving an empty string ('') as the parameter you can always test for:
unless params[:projectid].blank?
I don't think i undestood your question, but i hope this helps.
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...