ActiveRecord none? method fires query - ruby-on-rails

I have this code
def evaluate(collection)
if collection.none?
[]
else
collection.group(#group).pluck(*#columns)
end
end
The collection is an ActiveRecord::Relation object - for e.g. User.where(:name => 'Killer')
Now sometimes I also pass the Rails 4 none relation Users.none, that's why the check for none. If I do not check for none?, the call to pluck throws an arguments exception.
The problem is whenever I query any relation for none? it executes the query. See here:
> User.where(id: 1).none?
User Load (0.2ms) SELECT "users".* FROM "v1_passengers" WHERE "users"."id" = 1
=> false
> User.where(id: 1).none.none?
=> true
I do not want to execute to query just to check for none. Any workarounds?
Update: The none? method is actually array method thats why the query is executed. It's like calling to_a on the relation. What I want to know is how to figure out if the relation is a none

Found one method to do this without firing query. When you call none on a relation it appends the ActiveRecord::NullRelation to the extending_values array of the relation:
> User.where(id: 1).extending_values.include?(ActiveRecord::NullRelation)
=> false
> User.where(id: 1).none.extending_values.include?(ActiveRecord::NullRelation)
=> true

Not sure you can, there's no distinguishing feature between a Null Relation and an Actual Relation. Perhaps go down the rescue route:
begin
collection.group(#group).pluck(*#columns)
rescue #add exact Exception to catch
[]
end
Not exactly clean but gets round the problem

Related

Differences between `any?` and `exists?` in Ruby on Rails?

In Ruby on Rails, there appear to be two methods to check whether a collection has any elements in it.
Namely, they are ActiveRecord::FinderMethods’ exists? and ActiveRecord::Relation’s any?. Running these in a generic query (Foo.first.bars.exists? and Foo.first.bars.any?) generated equivalent SQL. Is there any reason to use one over the other?
#any and #exists? are very different beasts but query similarly.
Mainly, #any? accepts a block — and with this block it retrieves the records in the relation, calls #to_a, calls the block, and then hits it with Enumerable#any?. Without a block, it's the equivalent to !empty? and counts the records of the relation.
#exists? always queries the database and never relies on preloaded records, and sets a LIMIT of 1. It's much more performant vs #any?. #exists? also accepts an options param as conditions to apply as you can see in the docs.
The use of ActiveRecord#any? is reduced over ActiveRecord#exists?. With any? you can check, in the case of passing a block, if certain elements in that array matches the criteria. Similar to the Enumerable#any? but don't confuse them.
The ActiveRecord#any? implements the Enumerable#any? inside the logic of its definition, by converting the Relation accessed to an array in case a block has been passed to it and yields and access the block parameters to implement in a "hand-made" way a "Ruby" any? method.
The handy else added is intended to return the negation of empty? applied to the Relation. That's why you can check in both ways if a model has or no records in it, like:
User.count # 0
User.any? # false
# SELECT 1 AS one FROM "users" LIMIT ? [["LIMIT", 1]]
User.exists? # false
# SELECT 1 AS one FROM "users" LIMIT ? [["LIMIT", 1]]
You could also check in the "any?" way, if some record attribute has a specific value:
Foo.any? { |foo| foo.title == 'foo' } # SELECT "posts".* FROM "posts"
Or to save "efficiency" by using exists? and improve your query and lines of code:
Foo.exists?(title: 'foo') # SELECT 1 AS one FROM "posts" WHERE "posts"."title" = ? LIMIT ? [["title", "foo"], ["LIMIT", 1]]
ActiveRecord#exists? offers many implementations and is intended to work in a SQL level, rather than any?, that anyways will convert the Relation what you're working with in an array if you don't pass a block.
The answers here are all based on very outdated versions. This commit from 2016 / ActiveRecord 5.1 changes empty?, which is called by any? when no block is passed, to call exists? when not preloaded. So in vaguely-modern Rails, the only difference when no block is passed is a few extra method calls and negations, and ignoring preloaded results.
I ran into a practical issue: exists? forces a DB query while any? doesn't.
user = User.new
user.skills = [Skill.new]
user.skills.any?
# => true
user.skills.exists?
# => false
Consider having factories and a before_create hook:
class User < ActiveRecord::Base
has_many :skills
before_create :ensure_skills
def ensure_skills
# Don't want users without skills
errors.add(:skills, :invalid) if !skills.exists?
end
end
FactoryBot.define do
factory :user do
skills { [association(:skill)] }
end
end
create(:user) will fail, because at the time of before_create skills are not yet persisted. Using .any? will solve this.

'Length' method of an ActiveRecord query returns error when the size of the result is 1. How can I check if this is the case?

If I do this:
a = User.all
a.length # 15
It works, I get 15. But, if for some reason my query returns only one value ...
a = User.find(1)
a.length # NoMethodError: undefined method `length' for #<User:0x007f00815f9e38>
Which is logical, there's only one result. Is there a method that can check if the result is 1?
EDIT:
I'll explain what I'm trying to do. I want to know if a certain query returns 1 or more records. I'd try this with count, length or size, but all of those methods aren't available when the query returns only a single record. I need to know when I get one record and when there's more, but I can't figure out how to measure this without getting 'nomethod' errors on both scenarios.
# There're 15 users active right now.
a=User.find_by(:active => true)
a.length # 15
# Only 1 user is active.
a=User.find_by(:active => true)
a.length # Nomethod Error
SOLUTION:
I was using the find_by method, which doesn't seem to include the size, count, length methods in the result. As suggested below, I used where instead, and the result now included the size methods.
When you call
User.all
the result is an array, and thus has a length method.
In contrast, when you call either of
User.find(1)
or
User.find_by(active: true)
the result is either a single User instance or nil, neither of which have a length method. Since find does a lookup against the backing table's unique id, you'll never get back more than one result.
As a further contrast, if your User model defined a boolean called active you could write:
User.where(active: true)
... and that would return an array that you can use with count or length.
In general, when you're looking for the number of rows that correspond to some condition, I would use
Model.where(conditions).count
... as this is reliable and generates a single query (SELECT COUNT(*) ...) without instantiating any models.
Try using count. The difference between them is described here.
Doing Foo.find(:id) you're basically telling ActiveRecord you just want to know about that 1 foo, you're not bothered about it's size or count because you already know it.
This is my irb:
irb(main):001:0> u = User.all
irb(main):002:0> u.count
(0.5ms) SELECT COUNT(*) FROM `users`
=> 18
irb(main):003:0> u.length
=> 18
irb(main):004:0> u.size
=> 18
All great, I do have 18 users.
This is my console when I try to do a count from a find
irb(main):005:0> u1 = User.find(1)
irb(main):006:0> u1.length
NoMethodError: undefined method `length' for #<User:0x00000002dbc9a8>
Smashing, we're on the same page now.
Now, this is what happens when I have 1 user from a collection/array.
irb(main):031:0> u.count
(0.4ms) SELECT COUNT(*) FROM `users`
=> 1
irb(main):032:0> u.size
=> 1
irb(main):033:0> u.length
=> 1
irb(main):034:0>
Becaue I'm querying a collection/array of records instead of a single one, I don't get the error.
So, Foo.all will return an array and give you access to count, size and length while Foo.find returns a single object.
Edit:
If find_by returns nil or undefined method you could write your query like so: Foo.where(active: true) ..

ActiveRecord .where() method ignored

I've got a weird issue... In a model User, that has no relation with the model Interest, I try to call this query:
# file model.rb
def self.my_func
Interest.where('id IN (?)', [1,2])
end
But it's completely ignored... and if I replace by this:
# file model.rb
def self.my_func
Interest.find(1)
end
the .find() method is triggered and I get result.
If I directly call Interest.where('id IN (?)', [1,2]) in rails console it works...
I'm on RoR 3.2.13
Any idea?
Thank you all.
Rails doesn't evaluate the query until you actually try to access the results. Calling Model.where just returns an ActiveRecord::Relation onto which you can chain additional where/order/etc calls.
Try this (.all forces the query to be evaluated and returns an array of results):
def self.my_func
Interest.where(id: [1,2]).all
end
Note that should shouldn't actually do this. It's much better for your model to just allow the ActiveRecord::Relation to be returned from the method, so the calling code can apply additional scoping/ordering methods to it.
Also note that, instead of manually building an id in (?) query, Rails is smart enough to do it for you if you just use where(id: [1,2]).
Bonus notes:
On the terminal, the results get evaluated from Model.where immediately because IRB invokes inspect on the result of each expression you enter so it has something to print, and the returned ActiveRecord::Relation evaluates its query when inspected. You can bypass this to prove the point to yourself by adding ;nil, so that your statement evaluates to nil; note that the SELECT doesn't happen until I manually call x.inspect:
irb(main):008:0> x = User.where("name like 'bob'"); nil
=> nil
irb(main):009:0> x.inspect
User Load (0.5ms) SELECT "users".* FROM "users" WHERE (name like 'bob')
=> "[]"

Why is ActiveRelation returning an array when I call merge?

I have models called User, Activity, and Task and I can do the following
> puts Activity.joins(:task).to_sql
SELECT "activities".* FROM "activities" INNER JOIN "tasks" ON "tasks"."id" = "activities"."task_id" ORDER BY date DESC
> puts User.first.shared_tasks.to_sql
SELECT "tasks".* FROM "tasks" INNER JOIN "user_tasks" ON "tasks"."id" = "user_tasks"."task_id" WHERE "user_tasks"."user_id" = 1
But when I try to merge the two, I get an array:
> puts Activity.joins(:task).merge(User.first.shared_tasks).to_sql
NoMethodError: undefined method `to_sql' for []:Array
Why is that not returning a relation? I need to put a where clause on it.
Update:
Upon further inspection, it looks likes User.first.shared_tasks is being evaluated to an array of tasks immediately. I can gett the behavior I want by adding an order call:
> puts Activity.joins(:task).merge(User.first.shared_tasks.order()).to_sql
SELECT "activities".* FROM "activities" INNER JOIN "tasks" ON "tasks"."id" = "activities"."task_id" INNER JOIN "user_tasks" ON "tasks"."id" = "user_tasks"."task_id" WHERE "user_tasks"."user_id" = 1 ORDER BY date DESC
Is there a better way to prevent that relation from being evaluated besides adding an empty order?
Whenever you call a method on a relation object that it doesn't respond to, it delegates that method to some other type of object. In the case of merge, it will be converted to an array. This is done via the ActiveRecord::Delegation module. Some methods such as each are explicitly delegated, and others such as merge are done through method_missing.
For example, if you do:
Activity.joins(:task).each { |activity| puts activity }
the relation delegates .each to an array (Enumerator), essentially doing a conversion.
Alright, I still have not figured out why User.first.shared_tasks is being immediately evaluated, but I have figured out a work around. I can just call scoped:
> User.first.shared_tasks.class
=> Array
> User.first.shared_tasks.scoped.class
=> ActiveRecord::Relation
Now when I try to do the merge:
Activity.joins(:task).merge(User.first.shared_tasks.scoped)
It uses ActiveRecord::Relation#merge instead of Array#merge
For relation you can try this
puts (Activity.joins(:task) & User.first.shared_tasks).to_sql
merge helps in create a scope and it returns an array of resulted objects.

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

Resources