Rails order query method for multiple and child attributes - ruby-on-rails

While the new rails syntax implements an attribute that chooses the order direction as a symbol with colons,
I have not found a way to enact this syntaxic method for the following old-school style of ordering, when invoking multiple attributes where at least one is from a child table
#items = Item.eager_load(:itemoptions)
.order('model_id ASC, itemoptions.modeloption_id ASC')
.where( [...])
.all
how can this be achieved with the new approach?

As you're joining both tables, you need to prefix the columns you're using with their corresponding table names.
The only arguments for direction that order is able to receive are [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"], so that forces you to pass the columns as hash keys in the form { "table_name.column_name" => <direction> }.
Try with:
Item.eager_load(:itemoptions)
.where( [...])
.order('items.id' => :ASC, 'itemoptions.modeloption_id' => :ASC)
You don't need the all method there.

The error message that I receive says that it's possible to use Arel's sql escape to do this. Currently I am able to do something like Item.eager_load(:itemoptions).order(Arel.sql("model_id ASC, itemoptions.modeloption_id ASC"))
Also I know not part of the question, but I don't think you need the .all at the end

Related

Are the .order method parameters in ActiveRecord sanitized by default?

I'm trying to pass a string into the .order method, such as
Item.order(orderBy)
I was wondering if orderBy gets sanitized by default and if not, what would be the best way to sanitize it.
The order does not get sanitized. This query will actually drop the Users table:
Post.order("title; drop table users;")
You'll want to check the orderBy variable before running the query if there's any way orderBy could be tainted from user input. Something like this could work:
items = Item.scoped
if Item.column_names.include?(orderBy)
items = items.order(orderBy)
end
They are not sanitized in the same way as a .where clause with ?, but you can use #sanitize_sql_for_order:
sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
# => "field(id, 1,3,2)"
sanitize_sql_for_order("id ASC")
# => "id ASC"
http://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_for_order
Just to update this for Rails 5+, as of this writing, passing an array into order will (attempt to) sanitize the right side inputs:
Item.order(['?', "'; DROP TABLE items;--"])
#=> SELECT * FROM items ORDER BY '''; DROP TABLE items;--'
This will trigger a deprecation warning in Rails 5.1 about a "Dangerous query method" that will be disallowed in Rails 6. If you know the left hand input is safe, wrapping it in an Arel.sql call will silence the warning and, presumably, still be valid in Rails 6.
Item.order([Arel.sql('?'), "'; DROP TABLE items;--"])
#=> SELECT * FROM items ORDER BY '''; DROP TABLE items;--'
It's important to note that unsafe SQL on the left side will be sent to the database unmodified. Exercise caution!
If you know your input is going to be an attribute of your model, you can pass the arguments as a hash:
Item.order(column_name => sort_direction)
In this form, ActiveRecord will complain if the column name is not valid for the model or if the sort direction is not valid.
I use something like the following:
#scoped = #scoped.order Entity.send(:sanitize_sql, "#{#c} #{#d}")
Where Entity is the model class.
Extend ActiveRecord::Relation with sanitized_order.
Taking Dylan's lead I decided to extend ActiveRecord::Relation in order to add a chainable method that will automatically sanitize the order params that are passed to it.
Here's how you call it:
Item.sanitized_order( params[:order_by], params[:order_direction] )
And here's how you extend ActiveRecord::Relation to add it:
config/initializers/sanitized_order.rb
class ActiveRecord::Relation
# This will sanitize the column and direction of the order.
# Should always be used when taking these params from GET.
#
def sanitized_order( column, direction = nil )
direction ||= "ASC"
raise "Column value of #{column} not permitted." unless self.klass.column_names.include?( column.to_s )
raise "Direction value of #{direction} not permitted." unless [ "ASC", "DESC" ].include?( direction.upcase )
self.order( "#{column} #{direction}" )
end
end
It does two main things:
It ensures that the column parameter is the name of a column name of the base klass of the ActiveRecord::Relation.
In our above example, it would ensure params[:order_by] is one of Item's columns.
It ensures that the direction value is either "ASC" or "DESC".
It can probably be taken further but I find the ease of use and DRYness very useful in practice when accepting sorting params from users.

Rails gem rails3-jquery-autocomplete: How do I query multiple fields

I'm using the rails3-jquery-autocomplete gem found here: http://github.com/crowdint/rails3-jquery-autocomplete
The instructions are clear for how to query a single attribute of a model and I am able to make that work without a problem.
My Person model has two attributes that I would like to combine and query, however. They are first_name and last_name. I would like to combine them into a pseudo-attribute called full_name. Currently, I receive this error:
ActiveRecord::StatementInvalid (SQLite3::SQLException: no such column: full_name: SELECT "people".* FROM "people" WHERE (LOWER(full_name) LIKE 'cla%') ORDER BY full_name ASC LIMIT 10):
There is no full_name attribute of the Person model, though I have the following method in the Person model file:
def full_name
"#{self.first_name} #{self.last_name}"
end
How do I modify the Person model file so that calls to full_name queries the database to match a combination of first_name and last_name?
Your pseudo attribute works only on records already retrieved, but it has no bearing on searching for records. Probably the easiest solution is a named named scope like:
scope :search_by_name, lambda { |q|
(q ? where(["first_name LIKE ? or last_name LIKE ? or concat(first_name, ' ', last_name) like ?", '%'+ q + '%', '%'+ q + '%','%'+ q + '%' ]) : {})
}
Thus, a call like:
Person.search_by_name(params[:q])
will return an appropriate result set. It will also return all entries if no param was passed (or more specifically, this scope will add nothing extra), making it an easy drop-in for the index action.
Sadly, the scope method mentioned above didn't work for me. My solution was to simply overwrite the get_autocomplete_items method (formerly get_items).
For what it's worth, there is a MySQL function (other db's have it as well, but we're talking MySQL for the moment) that is better suited to the type of concatenation you're using:
def get_autocomplete_items(parameters)
items = Contact.select("DISTINCT CONCAT_WS(' ', first_name, last_name) AS full_name, first_name, last_name").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
end
MySQL's CONCAT_WS() is intended to join strings together with some sort of separator and is ideal for full names.
This chunk of code basically says return the "first last" formatted names of contacts that match whatever the user is searching by when we pair up the database's contact records by a concatenated pairs of first and last names. I feel it's better than the SQL statement above since it does a full search that will match first AND/OR last name in one statement, not three OR statements.
Using this "hn sm" would match "John Smith" since indeed "hm sm" is LIKE "John Smith". Furthermore, it has the added benefit of also returning the concatenated first and last name of each contact. You may want the full record. If that's the case, remove the select() query from the line above. I personally had the need for the user to search for a name and have an autocomplete field return all possible matches, not the records.
I know this is a bit late, but I hope it helps someone else!
Full implementation of multiple-field autocomplete :
Following my comment, my solution to integrate into jquery-autocomplete was to have a custom implementation of the "internal" autocomplete.
1. Query the database
If you're using ActiveRecord, you can use DGM's solution for your named_scope
If you're using Mongoid, you can use this syntax instead:
scope :by_first_name, ->(regex){
where(:first_name => /#{Regexp.escape(regex)}/i)
}
scope :by_last_name, ->(regex){
where(:last_name => /#{Regexp.escape(regex)}/i)
}
scope :by_name, ->(regex){
any_of([by_first_name(regex).selector, by_last_name(regex).selector])
}
EDIT : if you want a stronger autocomplete that can handle accents, matching parts of text, etc; you should try the mongoid text index. Credits to the original answer there
index(first_name: 'text', last_name: 'text')
scope :by_name, ->(str) {
where(:$text => { :$search => str })
}
And don't forget to build the indexes after adding that rake db:mongoid:create_indexes
So you can basically do User.by_name(something)
2. Create an autocomplete action in your controller
Because the one provided by jquery-autocomplete... ain't gonna do what we want.
Note that you'll have to convert the result to JSON so it can be used in the frontend jquery-autocomplete. For this I have chosen to use the gem ActiveModel::Serializer, but feel free to use something else if you prefer, and skip step 3
In your controller :
def autocomplete
#users = User.by_name(params[:term])
render json: #users, root: false, each_serializer: AutocompleteSerializer
end
3. Reformat the response
Your serializer using the gem activemodel:
I provided the link to the 0.9 version, as the master mainpage doesn't contain the full documentation.
class AutocompleteSerializer < ActiveModel::Serializer
attributes :id, :label, :value
def label
object.name
# Bonus : if you want a different name instead, you can define an 'autocomplete_name' method here or in your user model and use this implementation
# object.respond_to?('autocomplete_name') ? object.autocomplete_name : object.name
end
def value
object.name
end
4. Create a route for your autocompletion
In your routes :
get '/users/autocomplete', to: 'users#autocomplete', as: 'autocomplete_user'
5. Have fun in your views
Finally in your views you can use the default syntax of jquery-rails, but remember to change the path !
<%= form_tag '' do
autocomplete_field_tag 'Name', '', autocomplete_user_path, :id_element => "#{your_id}", class: "form-control"
end %>
RQ : I used some 2-level deep nested forms, so it was a bit tricky to get the right id element your_id. In my case I had to do somethig complicated, but most likely it will be simple for you. You can always have a look at the generated DOM to retrieve the field ID
This is a hack and I would very much like this function to be included in the gem but as slant said overwriting the get_autocomplete_items works as he wrote it but it will only return first_name and last_name from the model column. In order to restore functionality that frank blizzard asked for you also need to return the id of the row.
items = Contact.select("DISTINCT CONCAT_WS(' ', first_name, last_name) AS full_name, first_name, last_name, id").where(["CONCAT_WS(' ', first_name, last_name) LIKE ?", "%#{parameters[:term]}%"])
The difference between mine and slant's answer is id as the last argument of the select method. I know it is late but I hope it helps somebody in the future.
If you don't need the functionality of comparing your search string against the concatenated string and are trying to just do a query on three separate columns you can use this fork of the gem : git://github.com/slash4/rails3-jquery-autocomplete.git. Oh also that fork will only work with ActiveRecord which is probably why they didn't pull it.
To perform a case-insensitive search using #Cyril's method for rails4-autocomplete, I did a slight modification to the named scope #DGM provided
scope :search_by_name, lambda { |q|
q.downcase!
(q ? where(["lower(first_name) LIKE ? or lower(last_name) LIKE ? or concat(lower(first_name), ' ', lower(last_name)) like ?", '%'+ q + '%', '%'+ q + '%','%'+ q + '%' ]) : {})
}
This converts the record's entry to lowercase and also converts the search string to lowercase as it does the comparison
An alternative to the previous suggestions is to define a SQL view on the table holding first_name and last_name. Your SQL code (in SQLite) might look like:
CREATE VIEW Contacts AS
SELECT user.id, ( user.first_name || ' ' || users.last_name ) AS fullname
FROM persons;
Then define a model for the table:
class Contact < ActiveRecord::Base
set_table_name "contacts"
end
You can now use rails3-jquery-autocomplete 'out-of-the box'. So in your controller you would write:
autocomplete :contact, :fullname
In your view file you can simply write:
f.autocomplete_field :contact, autocomplete_contact_fullname_path
I will leave configuration of the route as an exercise for the reader :-).

Ruby on Rails: how do I sort with two columns using ActiveRecord?

I want to sort by two columns, one is a DateTime (updated_at), and the other is a Decimal (Price)
I would like to be able to sort first by updated_at, then, if multiple items occur on the same day, sort by Price.
In Rails 4 you can do something similar to:
Model.order(foo: :asc, bar: :desc)
foo and bar are columns in the db.
Assuming you're using MySQL,
Model.all(:order => 'DATE(updated_at), price')
Note the distinction from the other answers. The updated_at column will be a full timestamp, so if you want to sort based on the day it was updated, you need to use a function to get just the date part from the timestamp. In MySQL, that is DATE().
Thing.find(:all, :order => "updated_at desc, price asc")
will do the trick.
Update:
Thing.all.order("updated_at DESC, price ASC")
is the Rails 3 way to go. (Thanks #cpursley)
Active Record Query Interface lets you specify as many attributes as you want to order your query:
models = Model.order(:date, :hour, price: :desc)
or if you want to get more specific (thanks #zw963 ):
models = Model.order(price: :desc, date: :desc, price: :asc)
Bonus: After the first query, you can chain other queries:
models = models.where('date >= :date', date: Time.current.to_date)
Actually there are many ways to do it using Active Record. One that has not been mentioned above would be (in various formats, all valid):
Model.order(foo: :asc).order(:bar => :desc).order(:etc)
Maybe it's more verbose, but personally I find it easier to manage.
SQL gets produced in one step only:
SELECT "models".* FROM "models" ORDER BY "models"."etc" ASC, "models"."bar" DESC, "models"."foo" ASC
Thusly, for the original question:
Model.order(:updated_at).order(:price)
You need not declare data type, ActiveRecord does this smoothly, and so does your DB Engine
Model.all(:order => 'updated_at, price')
None of these worked for me!
After exactly 2 days of looking top and bottom over the internet, I found a solution!!
lets say you have many columns in the products table including: special_price and msrp. These are the two columns we are trying to sort with.
Okay, First in your Model
add this line:
named_scope :sorted_by_special_price_asc_msrp_asc, { :order => 'special_price asc,msrp asc' }
Second, in the Product Controller, add where you need to perform the search:
#search = Product.sorted_by_special_price_asc_msrp_asc.search(search_params)

"Section" has_many versioned "Articles" -- How can I get the freshest subset?

I have a Model called Section which has many articles (Article). These articles are versioned (a column named version stores their version no.) and I want the freshest to be retrieved.
The SQL query which would retrieve all articles from section_id 2 is:
SELECT * FROM `articles`
WHERE `section_id`=2
AND `version` IN
(
SELECT MAX(version) FROM `articles`
WHERE `section_id`=2
)
I've been trying to make, with no luck, a named scope at the Article Model class which look this way:
named_scope :last_version,
:conditions => ["version IN
(SELECT MAX(version) FROM ?
WHERE section_id = ?)", table_name, section.id]
A named scope for fetching whichever version I need is working greatly as follows:
named_scope :version, lambda {|v| { :conditions => ["version = ?", v] }}
I wouldn't like to end using find_by_sql as I'm trying to keep all as high-level as I can. Any suggestion or insight will be welcome. Thanks in advance!
I would take a look at some plugins for versioning like acts as versioned or version fu or something else.
If you really want to get it working the way you have it now, I would add a boolean column that marks if it is the most current version. It would be easy to run through and add that for each column and get the data current. You could easily keep it up-to-date with saving callbacks.
Then you can add a named scope for latest on the Articles that checks the boolean
named_scope :latest, :conditions => ["latest = ?", true]
So for a section you can do:
Section.find(params[:id]).articles.latest
Update:
Since you can't add anything to the db schema, I looked back at your attempt at the named scope.
named_scope :last_version, lambda {|section| { :conditions => ["version IN
(SELECT MAX(version) FROM Articles
WHERE section_id = ?)", section.id] } }
You should be able to then do
section = Section.find(id)
section.articles.last_version(section)
It isn't the cleanest with that need to throw the section to the lambda, but I don't think there is another way since you don't have much available to you until later in the object loading, which is why I think your version was failing.

How do I find() all records unique in certain fields?

I have a list of 'request' objects, each of which has fairly normal activerecord qualities. The requests table is related to the games table with a join table, 'games_requests,' so that a request has a request.games array.
The question is, is there a way to do a find for the last n unique requests, where uniqueness is defined by the games column and a couple others, but specifically ignores other colums (like the name of the requesting user?)
I saw a syntax like 'find (:all, :limit=>5, :include=>[:games,:stage])' but that was returning duplicates.
Thanks...
EDIT: Thanks to chaos for a great response. You got me really close, but I still need the returns to be valid request objects: the first 5 records that are distinct in the requested rows. I could just use the find as you constructed it and then do a second find for the first row in the table that matches each of the sets returned by the first find.
EDIT:
Games.find(
:all, :limit => 5,
:include => [:games, :requests],
:group => 'games, whatever, whatever_else'
)
...gives an SQL error:
Mysql::Error: Unknown column 'games' in 'group statement': SELECT * FROM `games` GROUP BY games
I made a few changes for what I assumed to be correct for my project; getting a list of requests instead of games, etc:
Request.find(
:all, :order=>"id DESC", :limit=>5,
:include=>[:games], #including requests here generates an sql error
:group=>'games, etc' #mysql error: games isn't an attribute of requests
:conditions=>'etc'
)
I'm thinking I'm going to have to use the :join=> option here.
Games.find(
:all, :limit => 5,
:include => [:games, :requests],
:group => 'games, whatever, whatever_else'
)
Try Rails uniq_by.It also works with association and returns array.
#document = Model.uniq_by(&:field)
More Detail
I think you'll be able to do this using find_by_sql and GROUP BY:
Games.find_by_sql("SELECT * FROM games GROUP BY user_id")

Resources