ActiveRelation uniquness by column - ruby-on-rails

SO i have active relation like:
[[id:1, post_id: 10, user_id:2][id:2, post_id:11, user_id:2]]
When i say relation.uniq, it doesn't return one value, because id's of objects are different(which is obvious behavior).
How do i get only unique object, but not by full fields, but only by some specific field, for example (:user_id)
I've tried to use distinct, but that returns me only array of fields, instead of active relation or objects
As result, i'd want to get an active relation where, field user_id would be unique
Though i'm not sure if this is good idea, maybe i should use some other methodics to get what i want, because i'm starting to understand, that this is not the best idea

How did you use distinct? When I run
> Model.select('DISTINCT column')
it seems to return an object of class ActiveRecord::Relation.

Use pluck.
Model.distinct.pluck(:user_id)
Examples:
Person.pluck(:id)
# SELECT people.id FROM people
# => [1, 2, 3]
Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']

Related

Getting ids from both tables of a join ActiveRecord_AssociationRelation

Let's say I have a ActiveRecord_AssociationRelation named joined obtained from joining 2 tables users and posts.
I would like to select the ids from both the user and the post and place them turn them to a hash by calling as_json.
Basically I would like to do something like this:
joined.select('users.id','posts.id')
# joined.select('posts.id','users.id')
joined.as_json
The code above returns only the last id argument mentioned in the select method( the post id in the first case and the user id in the commented case).
I would like to know if it's possible to get both ids or if Rails's Convention over Configuration principles restrict us to get both in this way( so that we might return the :user_id column instead for example)?
Just use:
joined.pluck('users.id','posts.id')
it will return something like that:
=> [[1, 97], [2, 97], [3, 97]]
Could do something like this:
joined.to_a.map{ |j| { user_id: j['users.id'], post_id: j['posts.id']} }

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 3 - Expression-based Attribute in Model

How do I define a model attribute as an expression of another attribute?
Example:
Class Home < ActiveRecord::Base
attr_accessible :address, :phone_number
Now I want to be able to return an attribute like :area_code, which would be an sql expression like "substr(phone_number, 1,3)".
I also want to be able to use the expression / attribute in a group by query for a report.
This seems to perform the query, but does not return an object with named attributes, so how do I use it in a view?
Rails Console:
#ac = Home.group("substr(phone_number, 1,3)").count
=> #<OrderedHash {"307"=>3, "515"=>1}>
I also expected this to work, but not sure what kind of object it is returning:
#test = Home.select("substr(phone_number, 1,3) as area_code, count(*) as c").group("substr(phone_number, 1,3)")
=> [#<Home>, #<Home>]
To expand on the last example. Here it is with Active Record logging turned on:
>Home.select("substr(phone_number, 1,3) as area_code, count(*) as c").group("substr(phone_number, 1,3)")
Output:
Home Load (0.3ms) SELECT substr(phone_number, 1,3) as area_code, count(*) as c FROM "homes" GROUP BY substr(phone_number, 1,3)
=> [#<Home>, #<Home>]
So it is executing the query I want, but giving me an unexpected data object. Shouldn't I get something like this?
[ #<area_code: "307", c: 3>, #<area_code: "515", c: 1> ]
you cannot access to substr(...) because it is not an attribute of the initialized record object.
See : http://guides.rubyonrails.org/active_record_querying.html "selecting specific fields"
you can workaround this this way :
#test = Home.select("substr(phone_number, 1,3) as phone_number").group(:phone_number)
... but some might find it a bit hackish. Moreover, when you use select, the records will be read-only, so be careful.
if you need the count, just add .count at the end of the chain, but you will get a hash as you already had. But isn't that all you need ? what is your purpose ?
You can also use an area_code column that will be filled using callbacks on create and update, so you can index this column ; your query will run fast on read, though it will be slower on insertion.

Rails: Why can't you set an association to nil in a where clause?

I have photos that belong to collections and users. Photos always belong to a user, but may not be assigned to a collection.
In my controller, this works perfectly:
#collection_photos = Photo.where( :collection => #collection, :user => current_user )
However, this fails...
#other_photos = Photo.where( :collection => nil, :user => current_user )
...but this works:
#other_photos = Photo.where( :collection_id => nil, :user => current_user )
When collection is set to nil I get this error message: No attribute named 'collection' exists for table photos.
If I pass an object, it knows to search for collection_id from the symbol :collection, but if I don't pass an object it doesn't seem to be aware of the association.
Am I understanding this correctly? Could anyone explain a little better why :collection=>nil doesn't work?
when you use pass in the conditions into ActiveRecord, it actually tries to analyze the objects that you passed in, is it a string? an array? a hash? and what's in the string, array or hash?
and in your case, a hash, so it's trying to analyze what's in the hash, in the first statement (which works), you passed in a model instance as the value, so it tries to find if there are any associations that mapped to the key your specified and voila, it found it and everything works as planned
in the second case, you passed in nil as the value, now, ActiveRecord sees that it's a nil object, so it decided that it's not an association. note that it doesn't look at the key, but it only looked at the value, thus it tries to find if there's any column that mapped to the key, but it couldn't find, returning an error
in the last case, you passed in nil as the value, same thing, it tried to find a column which mapped to :collection_id, thus it passed in nil as the value in the SQL statement, and it returned successfully
so it's just an unfortunate considerations taken by ActiveRecord that makes the second case not working =)
hope this clarifies! =D
My guess is that it's like the famous rails .find vs .find_by_id.
.find is designed to throw an exception if it cannot find any association.
where as .find_by_id will just return nil if doesn't find any association.
so in your .where statement, when you search for the collection it's probably treating that like a .find and when you search by collection_id it will return nil just like .find_by_id does if it can't find any associated collection.
I'm not sure how these two methods differ in Activerecord's inner workings, but they are designed to react differently to nil results.
I think your answer is in ActiveRecord::PredicateBuilder.build_from_hash. There is a case statement in there that checks the class of each value in the hash, and it specifically looks for ActiveRecord::Relation
This seems to no longer be an issue in Rails 4. For instance the following code
#other_photos = Photo.where( :collection => nil, :user => User.first )
would run
User Load (Xms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
Photo Load (Xms) SELECT "photos".* FROM "photos" WHERE "photos"."collection_id" IS NULL AND "photos"."user_id" = 1
*Tested in Rails 4.1.1

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