Should I query using objects or their IDs? [duplicate] - sql

I am new to rails. What I see that there are a lot of ways to find a record:
find_by_<columnname>(<columnvalue>)
find(:first, :conditions => { <columnname> => <columnvalue> }
where(<columnname> => <columnvalue>).first
And it looks like all of them end up generating exactly the same SQL. Also, I believe the same is true for finding multiple records:
find_all_by_<columnname>(<columnvalue>)
find(:all, :conditions => { <columnname> => <columnvalue> }
where(<columnname> => <columnvalue>)
Is there a rule of thumb or recommendation on which one to use?

where returns ActiveRecord::Relation
Now take a look at find_by implementation:
def find_by
where(*args).take
end
As you can see find_by is the same as where but it returns only one record. This method should be used for getting 1 record and where should be used for getting all records with some conditions.

Edit:
This answer is very old and other, better answers have come up since this post was made. I'd advise looking at the one posted below by #Hossam Khamis for more details.
Use whichever one you feel suits your needs best.
The find method is usually used to retrieve a row by ID:
Model.find(1)
It's worth noting that find will throw an exception if the item is not found by the attribute that you supply. Use where (as described below, which will return an empty array if the attribute is not found) to avoid an exception being thrown.
Other uses of find are usually replaced with things like this:
Model.all
Model.first
find_by is used as a helper when you're searching for information within a column, and it maps to such with naming conventions. For instance, if you have a column named name in your database, you'd use the following syntax:
Model.find_by(name: "Bob")
.where is more of a catch all that lets you use a bit more complex logic for when the conventional helpers won't do, and it returns an array of items that match your conditions (or an empty array otherwise).

Model.find
1- Parameter: ID of the object to find.
2- If found: It returns the object (One object only).
3- If not found: raises an ActiveRecord::RecordNotFound exception.
Model.find_by
1- Parameter: key/value
Example:
User.find_by name: 'John', email: 'john#doe.com'
2- If found: It returns the object.
3- If not found: returns nil.
Note: If you want it to raise ActiveRecord::RecordNotFound use find_by!
Model.where
1- Parameter: same as find_by
2- If found: It returns ActiveRecord::Relation containing one or more records matching the parameters.
3- If not found: It return an Empty ActiveRecord::Relation.

There is a difference between find and find_by in that find will return an error if not found, whereas find_by will return null.
Sometimes it is easier to read if you have a method like find_by email: "haha", as opposed to .where(email: some_params).first.

Since Rails 4 you can do:
User.find_by(name: 'Bob')
which is the equivalent find_by_name in Rails 3.
Use #where when #find and #find_by are not enough.

The accepted answer generally covers it all, but I'd like to add something,
just incase you are planning to work with the model in a way like updating, and you are retrieving a single record(whose id you do not know), Then find_by is the way to go, because it retrieves the record and does not put it in an array
irb(main):037:0> #kit = Kit.find_by(number: "3456")
Kit Load (0.9ms) SELECT "kits".* FROM "kits" WHERE "kits"."number" =
'3456' LIMIT 1
=> #<Kit id: 1, number: "3456", created_at: "2015-05-12 06:10:56",
updated_at: "2015-05-12 06:10:56", job_id: nil>
irb(main):038:0> #kit.update(job_id: 2)
(0.2ms) BEGIN Kit Exists (0.4ms) SELECT 1 AS one FROM "kits" WHERE
("kits"."number" = '3456' AND "kits"."id" != 1) LIMIT 1 SQL (0.5ms)
UPDATE "kits" SET "job_id" = $1, "updated_at" = $2 WHERE "kits"."id" =
1 [["job_id", 2], ["updated_at", Tue, 12 May 2015 07:16:58 UTC +00:00]]
(0.6ms) COMMIT => true
but if you use where then you can not update it directly
irb(main):039:0> #kit = Kit.where(number: "3456")
Kit Load (1.2ms) SELECT "kits".* FROM "kits" WHERE "kits"."number" =
'3456' => #<ActiveRecord::Relation [#<Kit id: 1, number: "3456",
created_at: "2015-05-12 06:10:56", updated_at: "2015-05-12 07:16:58",
job_id: 2>]>
irb(main):040:0> #kit.update(job_id: 3)
ArgumentError: wrong number of arguments (1 for 2)
in such a case you would have to specify it like this
irb(main):043:0> #kit[0].update(job_id: 3)
(0.2ms) BEGIN Kit Exists (0.6ms) SELECT 1 AS one FROM "kits" WHERE
("kits"."number" = '3456' AND "kits"."id" != 1) LIMIT 1 SQL (0.6ms)
UPDATE "kits" SET "job_id" = $1, "updated_at" = $2 WHERE "kits"."id" = 1
[["job_id", 3], ["updated_at", Tue, 12 May 2015 07:28:04 UTC +00:00]]
(0.5ms) COMMIT => true

Apart from accepted answer, following is also valid
Model.find() can accept array of ids, and will return all records which matches.
Model.find_by_id(123) also accept array but will only process first id value present in array
Model.find([1,2,3])
Model.find_by_id([1,2,3])

The answers given so far are all OK.
However, one interesting difference is that Model.find searches by id; if found, it returns a Model object (just a single record) but throws an ActiveRecord::RecordNotFound otherwise.
Model.find_by is very similar to Model.find and lets you search any column or group of columns in your database but it returns nil if no record matches the search.
Model.where on the other hand returns a Model::ActiveRecord_Relation object which is just like an array containing all the records that match the search. If no record was found, it returns an empty Model::ActiveRecord_Relation object.
I hope these would help you in deciding which to use at any point in time.

Suppose I have a model User
User.find(id)
Returns a row where primary key = id. The return type will be User object.
User.find_by(email:"abc#xyz.com")
Returns first row with matching attribute or email in this case. Return type will be User object again.
Note :- User.find_by(email: "abc#xyz.com") is similar to User.find_by_email("abc#xyz.com")
User.where(project_id:1)
Returns all users in users table where attribute matches.
Here return type will be ActiveRecord::Relation object. ActiveRecord::Relation class includes Ruby's Enumerable module so you can use it's object like an array and traverse on it.

Both #2s in your lists are being deprecated. You can still use find(params[:id]) though.
Generally, where() works in most situations.
Here's a great post: https://web.archive.org/web/20150206131559/http://m.onkey.org/active-record-query-interface

The best part of working with any open source technology is that you can inspect length and breadth of it.
Checkout this link
find_by ~> Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself. If no record is found, returns nil.
find ~> Finds the first record matching the specified conditions , but if no record is found, it raises an exception but that is done deliberately.
Do checkout the above link, it has all the explanation and use cases for the following two functions.

I will personally recommend using
where(< columnname> => < columnvalue>)

Related

Why does Postgres not accept my count column?

I am building a Rails app with the following models:
# vote.rb
class Vote < ApplicationRecord
belongs_to :person
belongs_to :show
scope :fulfilled, -> { where(fulfilled: true) }
scope :unfulfilled, -> { where(fulfilled: false) }
end
# person.rb
class Person < ApplicationRecord
has_many :votes, dependent: :destroy
def self.order_by_votes(show = nil)
count = 'nullif(votes.fulfilled, true)'
count = "case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end" if show
people = left_joins(:votes).group(:id).uniq!(:group)
people = people.select("people.*, COUNT(#{count}) AS people.vote_count")
people.order('people.vote_count DESC')
end
end
The idea behind order_by_votes is to sort People by the number of unfulfilled votes, either counting all votes, or counting only votes associated with a given Show.
This seem to work fine when I test against SQLite. But when I switch to Postgres I get this error:
Error:
PeopleControllerIndexTest#test_should_get_previously_on_show:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column people.vote_count does not exist
LINE 1: ...s"."show_id" = $1 GROUP BY "people"."id" ORDER BY people.vot...
^
If I dump the SQL using #people.to_sql, this is what I get:
SELECT people.*, COUNT(nullif(votes.fulfilled, true)) AS people.vote_count FROM "people" LEFT OUTER JOIN "votes" ON "votes"."person_id" = "people"."id" GROUP BY "people"."id" ORDER BY people.vote_count DESC
Why is this failing on Postgres but working on SQLite? And what should I be doing instead to make it work on Postgres?
(PS: I named the field people.vote_count, with a dot, so I can access it in my view without having to do another SQL query to actually view the vote count for each person in the view (not sure if this works) but I get the same error even if I name the field simply vote_count.)
(PS2: I recently added the .uniq!(:group) because of some deprecation warning for Rails 6.2, but I couldn't find any documentation for it so I am not sure I am doing it right, still the error is there without that part.)
Are you sure you're not getting a syntax error from PostgreSQL somewhere? If you do something like this:
select count(*) as t.vote_count from t ... order by t.vote_count
I get a syntax error before PostgreSQL gets to complain about there being no t.vote_count column.
No matter, the solution is to not try to put your vote_count in the people table:
people = people.select("people.*, COUNT(#{count}) AS vote_count")
...
people.order(vote_count: :desc)
You don't need it there, you'll still be able to reference the vote_count just like any "normal" column in people. Anything in the select list will appear as an accessor in the resultant model instances whether they're columns or not, they won't show up in the #inspect output (since that's generated based on the table's columns) but you call the accessor methods nonetheless.
Historically there have been quite a few AR problems (and bugs) in getting the right count by just using count on a scope, and I am not sure they are actually all gone.
That depends on the scope (AR version, relations, group, sort, uniq, etc). A defaut count call that a gem has to generically use on a scope is not a one-fit-all solution. For that known reason Pagy allows you to pass the right count to its pagy method as explained in the Pagy documentation.
Your scope might become complex and the default pagy collection.count(:all) may not get the actual count. In that case you can get the right count with some custom statement, and pass it to pagy.
#pagy, #records = pagy(collection, count: your_count)
Notice: pagy will efficiently skip its internal count query and will just use the passed :count variable.
So... just get your own calculated count and pass it to pagy, and it will not even try to use the default.
EDIT: I forgot to mention: you may want to try the pagy arel extra that:
adds specialized pagination for collections from sql databases with GROUP BY clauses, by computing the total number of results with COUNT(*) OVER ().
Thanks to all the comments and answers I have finally found a solution which I think is the best way to solve this.
First of, the issue occurred when I called pagy which tried to count my scope by appending .count(:all). This is what caused the errors. The solution was to not create a "field" in select() and use it in .order().
So here is the proper code:
def self.order_by_votes(show = nil)
count = if show
"case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end"
else
'nullif(votes.fulfilled, true)'
end
left_joins(:votes).group(:id)
.uniq!(:group)
.select("people.*, COUNT(#{count}) as vote_count")
.order(Arel.sql("COUNT(#{count}) DESC"))
end
This sorts the number of people on the number of unfulfilled votes for them, with the ability to count only votes for a given show, and it works with pagy(), and pagy_arel() which in my case is a much better fit, so the results can be properly paginated.

Rails ActiveRecord query to match all params

I need to have an ActiveRecord Postgres query that returns results which match all the parameters passed in through an array.
Some background: I have a User model, which has many Topics (through Specialties). I'm passing in the Topic ids as a string (Parameters: {"topics"=>"1,8,3"}) and then turning them into an array with .split(',') so I end up with topic_params = ["1","8","3"].
Now I'm trying to return all Users who have Topics that match/include all of those. After following the answer in this question, I managed to return Users who match ANY of the Topics with this:
#users = User.includes(:topics, :organization).where(:topics => {:id => topic_params})
But I need it to return results that match ALL. I'd also be open to better ways to accomplish this sort of task overall.
One way would be something like this
User.joins(:topics).where(topics: { id: [1, 2, 3] }).group('users.id').having('count(distinct topics.id) = 3')
Obviously I haven't your exact schema so you might have to tweak it a bit, but this is the basic setup.
Important is that the having clause counter must match the number of items you're matching with.

JSONAPI Resources, need a filter for the last record

so I'm looking at creating a filter for a JSONAPI resource where I'll grab anything created within the last 10 minutes and return only one result if something's there.
Problem, when I call .last(1) on the returned records I get an error:
"exception": "undefined method `order' for #<Array:0x007fde4770bcf0>"
My non-breaking query is this:
records.where(user_id: user_id, updated_at: ten_minutes_ago..current_time)
Any help? I'd like to do this on the Rails-side rather than deal with a mangle of results (or no results) on our front-end. Thanks!
Okay, so the deal is this.
In order to grab only 1 record you'll probably have to overwrite/define your own paginator, like so:
paginator :none
I'd put it after your attributes and before your filters. Cool? Cool. Next, you can just do your basic Rails querying:
records.where(user_id: user_id, updated_at: ten_minutes_ago..current_time)
.order(id: :desc)
.limit(1)
You need to set your paginator or else the defaults or whatever you've defined will overwrite your limit query. Sweet. Easy.

findind all using .all vs. where

In my controller i am getting all entries form a table like this
#enums = Enuemerations.all
Then later i want to search and get the name from by doing
#enums.find(107).name
I get an error
undefined method `name' for #<Enumerator:0xb5eb9d98>
So I tried it out in the rails console and found this working
Enumeration.where(false).find(107)
where this does not work
Enumeration.all.find(107)
Can someone explain to me how this works?
Thanks
Using Enumeration.all instantly queries the database returning an Array with all the Enumeration records (if you only want a single record this would be very inefficient). It no longer knows about the ActiveRecord methods:
> Enumeration.all.class
Enumeration Load (0.1ms) SELECT "enumerations".* FROM "enumerations"
=> Array
Calling find on an Array uses Enumerable#find which would need a different syntax, e.g:
enums = Enumeration.all
enum = enums.find { |e| e.id == 2 }
=> #<Enumeration id: 2, name: "...">
Using Enumeration.where(false) only returns a lazy ActiveRecord::Relation, it doesn't actually hit the database (yet), this allows you to chain extra ActiveRecord methods such as find in your example above.
> Enumeration.where(false).class
=> ActiveRecord::Relation
> Enumeration.where(false).find(2)
Enumeration Load (0.2ms) SELECT "enumerations".* FROM "enumerations" WHERE "enumerations"."id" = ? LIMIT 1 [["id", 2]]
=> #<Enumeration id: 2, name: "...">

query in rails3.1

I got an issue in rails3.1, in the rails console
a=User.find_by_name 'castiel'
a.name #output: castiel
a=User.where "name like 'c%'"
a.name #output: User
#both query got the same thing,but why the second one output 'User'
I used a new feature in rails3.1 which is about authentication, you know the one that introduced in railscast episode authentication-in-rails3.1,maybe it's got something to do with this,just thought it might be good to let u know the circumstances here.
The issue is that find_by_name and where are not returning the same thing. find_by_name will find the first User that matches your query. where will return a collection of Users that match your query.
In your first example, the result is a single User object, so a.name gives you "castiel".
a=User.find_by_name 'castiel'
# 'a' will be something like #<User id:..., name: "castiel"...>
a.name #output: castiel
In the second example, however, the result is essentially an array of User objects. If you call a.name, it will return the name of the model it holds: User.
a=User.where "name like 'c%'"
# 'a' might be something like [#<User id:..., name: "castiel"...>]
a.name #output: User
Even though using where probably only returns you one result, it's still wrapped in an array.
That's because when you use where it will return a ActiveRecord::Relation object and not a User object. find_by_<blahblah> method returns an instance of the Model.