ActiveRecord pluck to SQL - ruby-on-rails

I know these two statements perform the same SQL:
Using select
User.select(:email)
# SELECT `users`.`email` FROM `users`
And using pluck
User.all.pluck(:email)
# SELECT `users`.`email` FROM `users`
Now I need to get the SQL statement derived from each method. Given that the select method returns an ActiveRecord::Relation, I can call the to_sql method. However, I cannot figure out how to get the SQL statement derived from a pluck operation on an ActiveRecord::Relation object, given that the result is an array.
Please, take into account that this is a simplification of the problem. The number of attributes plucked can be arbitrarily high.
Any help would be appreciated.

You cannot chain to_sql with pluck as it doesn't return ActiveRecord::relation. If you try to do, it throws an exception like so
NoMethodError: undefined method `to_sql' for [[""]]:Array
I cannot figure out how to get the SQL statement derived from a pluck
operation on an ActiveRecord::Relation object, given that the result
is an array.
Well, as #cschroed pointed out in the comments, they both(select and pluck) perform same SQL queries. The only difference is that pluck return an array instead of ActiveRecord::Relation. It doesn't matter how many attributes you are trying to pluck, the SQL statement will be same as select
Example:
User.select(:first_name,:email)
#=> SELECT "users"."first_name", "users"."email" FROM "users"
Same for pluck too
User.all.pluck(:first_name,:email)
#=> SELECT "users"."first_name", "users"."email" FROM "users"
So, you just need to take the SQL statement returned by the select and believe that it is the same for the pluck. That's it!

You could monkey-patch the ActiveRecord::LogSubscriber class and provide a singleton that would register any active record queries, even the ones that doesn't return ActiveRecord::Relation objects:
class QueriesRegister
include Singleton
def queries
#queries ||= []
end
def flush
#queries = []
end
end
module ActiveRecord
class LogSubscriber < ActiveSupport::LogSubscriber
def sql(event)
QueriesRegister.instance.queries << event.payload[:sql]
"#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
end
end
end
Run you query:
User.all.pluck(:email)
Then, to retrieve the queries:
QueriesRegister.instance.queries

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.

Order and limit clauses unexpectedly passed down to scope

(the queries here have no sensible semantic but I chose them for the sake of simplicity)
Project.limit(10).where(id: Project.select(:id))
generates as expected the following SQL query:
SELECT
"projects".*
FROM
"projects"
WHERE
"projects"."id" IN (
SELECT
"projects"."id"
FROM
"projects"
) LIMIT 10
But if I defined in my Project class the method
def self.my_filter
where(id: Project.select(:id))
end
Then
Project.limit(10).my_filter
generates the following query
SELECT
"projects".*
FROM
"projects"
WHERE
"projects"."id" IN (
SELECT
"projects"."id"
FROM
"projects" LIMIT 10
) LIMIT 10
See how the LIMIT 10 has now been also applied to the subquery.
Same issue when using a .order clause.
It happens with Rails 4.2.2 and Rails 3.2.20. It happens when the subquery is done on the Project table, it does happens if the subquery is done on another table.
Is there something I'm doing wrong here or do you think it is a Rails bug?
A workaround is to build my_filter by explicitly adding limit(nil).reorder(nil) to it but it is hackish.
EDIT: another workaround is to append the limit clause after the my_filter scope: Project.my_filter.limit(10).
This is actually a feature. Class methods work similar to scopes in ActiveRecord models.
And if you want to remove the already added scopes, you can either use unscoped, either call the method on a class directly, not on a scope:
def self.my_filter
unscoped.where(id: Project.select(:id))
end
# or
Project.my_filter
Your class method is applied in a way you may not be expecting:
Project.limit(10) # => a relation, not the Project class
.my_filter # => calling a class method on a relation
# Does, the following, suddenly:
# scoping { Project.my_filter }
# It's a relation's wrapper
From: .../ruby-2.0.0-p598/gems/activerecord-4.1.6/lib/active_record/relation.rb # line 281:
Owner: ActiveRecord::Relation
Visibility: public
Signature: scoping()
Scope all queries to the current scope.
Comment.where(post_id: 1).scoping do
Comment.first
end
# => SELECT "comments".* FROM "comments"
# WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
Please check unscoped if you want to remove all previous scopes (including
the default_scope) during the execution of a block.
Inside that scoping block, your class will include all the scoping rules of a relation it was built from into all queries, as scoping will enforce context. This is done so class methods can be properly chained, while still retaining the correct self. Of course, when you try using a class method inside the class method, stuff blows up.
In your first, "expected outcome" example, where is "natively" defined on relations, so no scope enforcement takes place: it's just not necessary.
Yeah, documentation hints that you can use unscoped in your nested query, like so:
def my_filter
where(id: Project.unscoped.select(:id))
end
...since that's where you need the "bare basis". Or, as you've already found out, you can just place limit at the end:
Project.my_filter.limit(10)
...here, at the time my_filter gets to execute, scoping will do effectively nothing: there will be no context built up to this point.

ActiveRecord none? method fires query

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

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.

Resources