Rails 3 get sql from scope - sql

Is there a way to get the sql for JUST a scope? So I want to do something like:
class Presentation < ActiveRecord::Base
has_many :calls
has_many :recordings, :through => :calls
scope :with_recordings, joins(:calls).joins(:recordings)
end
And then be able to get the sql for that scope.
Presentations.with_recordings.sql returns the entire sql statement, including the SELECT statement. All I want is the sql added by the scope. Figure there ought to be a way to do this.

I agree with ctcherry about this not being very useful, but having said that, I needed to do this for a project I was working on. We needed to duplicate the sql in the scopes to allow us to reuse the sql across different types of searches. Rather that have to maintain the same sql in two different places, I choose to extract the sql from the scope.
The code below is what I came up with. It's ugly, but works under Rails 3.0
def extract_named_scope_clause(scope, args)
# where_clauses will return an array of clauses for an entire relationship.
# As this is only run a single scope, we only ever care about the first.....
clause, *bind_vars = self.send(scope, args).where_clauses.first
# prefix 'and ' to the string, add some spaces and append any bind variables
if clause
[" and #{clause} ", bind_vars]
else
nil
end
end

This wouldn't really make sense, as there is no standard way to represent SQL "fragments".
The different kinds of SQL "fragments" that can be added and manipulated by a scope don't really have a clean way to be represented by themselves without being part of a complete SQL statement. A fragment could be "JOIN users ON users.id = orders.user_id" or it could be "WHERE active = 1". How would you return these without them being part of a complete SQL statement? This is most likely why there is no mechanism to retrieve them other than the one you have already discovered that just returns the complete SQL statement.

Related

Rails/ActiveRecord: Can I perform this query without passing the SQL string to #order?

I have two models Issue and Label. They have a many to many relationship.
I have a method that returns the ten labels that point to the most issues.
class Label < ApplicationRecord
has_many :tags
has_many :issues, through: :tags
def self.top
Label.joins(:issues)
.group(:name)
.order('count_id desc')
.count(:id)
.take(10)
end
end
It does exactly what I expect it to but I want to know if it's possible to compose the query without the SQL string.
order('count_id DESC') is confusing me. Where does count_id come from? There isn’t a column named count_id.
Label.joins(:issues).group(:name).column_names
#=> ["id", "name", "created_at", "updated_at"]
I’ve found some SQL examples here. I think it’s basically the same as ORDER BY COUNT(Id):
SELECT COUNT(Id), Country
FROM Customer
GROUP BY Country
ORDER BY COUNT(Id) DESC
Is it possible to perform the same query without passing in the SQL string? Can it be done with the ActiveRecord querying interface alone?
If you look at your query log, you'll see something like:
select count(labels.id) as count_id ...
The combination of your group call (with any argument) and the count(:id) call gets ActiveRecord to add the count_id column alias to the query. I don't think this is documented or specified anywhere (at least that I can find) but you can see it happen if you're brave enough to walk through the Active Record source.
In general, if you add a GROUP BY and then count(:x), Active Record will add a count_x alias. There's no column for this so you can't say order(:count_id), order(count_id: :desc), or any of the other common non-String alternatives. AFAIK, you have to use a string but you can wrap it in an Arel.sql to prevent future deprecation issues:
Label.joins(:issues)
.group(:name)
.order(Arel.sql('count_id desc'))
.count(:id)
.take(10)
There's no guarantee about this so if you use it, you should include something in your test suite to catch any problems if the behavior changes in the future.

Chaining scopes with joins

these two scopes don't seem to be chainable
scope :approved, ->{ with_stage(:approved)}
which in sql is
WHERE (pages.stage & 4 <> 0)
and
scope :with_galleries, ->{ joins("LEFT OUTER JOIN galleries ON galleries.galleriable_type = 'Brand' AND galleries.galleriable_id = page.brand_id").where("galleries.id is NOT NULL") }
this scope should give only the pages that have galleries (each page has one brand and each brand can have many galleries)
if I chain the :with_galleries, it seems that the rest of the conditions on pages table is lost
Am I doing the joins wrong?
You would get a more useful result if you let ActiveRecord do more of the heavy lifting for you. In particular, if you've set up associations properly, you should be able to write the following instead:
scope :with_galleries, joins(brand: :galleries)
... which would yield a properly chainable scope.
That would depend on two associations, one from your page model to the brand:
'belongs_to :brand'
and one from the brand to the galleries::
has_many :galleries, as: :galleriable
I'm inferring your model setup from the query that you've written, so I may have guessed wrong. But the basic principle here is to declare your associations and let ActiveRecord construct queries (unless your query is something very unusual, which yours is not -- you're just filtering depending on whether there are associated records, a common operation).
You need to construct the second scope using Arel. The simplest approach is to craft the full SQL statement you want the second scope to represent, and then paste it into http://www.scuttle.io/

Avoiding db hits in ActiveRecord

When I assign a database find to an instance variable in Rails, why do future requests to that variable also hit the database? Can this be avoided?
For example, I have 3 models: User, Resource, Opinion, with has_many :through on Opinion
#opinions = current_user.opinions # pulls in all of the user's opinions, which include respective resource ids
1. Calling for resource_id directly does not hit the database:
#opinions.each do |opinion|
opinion.resource_id # does not hit the database (as expected)
end
2. Performing a query does hit the database (even though variable has been assigned):
#opinions.find_by_resource_id(1) # DOES hit the database
Why does #2 hit the database? Is there a way to perform the same find without hitting the database?
The information is already contained in the #opinions variable, so a db call does not seem necessary.
If you don't need anything else in the #opinions array, I would scope your original query to only include opinions with that resource_id
#opinions = current_user.opinions.where("resource_id = ?", resource_id)
If you already have #opinions and just want to create a new array of objects that match for a specific key/value:
#opinions_with_resource_id = #opinions.select { |opinion| opinion.resource_id == 1234 }
Check out this other answer for another explanation or if you want to split the answer into multiple arrays.
Thoughts
Comment on your last piece of code
Methods like you called find_by_* are dynamic finders that use method_missing to hit the database and look inside of the column specified by the *.
Remaining comments from previous answer
If this object will ever need to access data on the Resource model, don't forget about the #includes() method, which will keep you from having to run additional queries down the road.
#opinions = current_user.opinions.includes(:resources)
See http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations

how do I write SQL in a ruby method?

I would like to have a method called feed in my User model that returns all the entries from two tables (discussions, exchanges).
In User.rb
def feed
SELECT * FROM discussions, exchanges GROUP BY created_at
end
This doesn't work, i get a problem in rails console
syntax error, unexpected ';', expecting '='
Can anyone show me how to write SQL in here? Basically I want to return and sort entries from two different tables..
if you want actual ActiveRecord objects you can try the following
def feed
exchanges = Exchange.all
discussions = Discussion.all
(exchanges + discussions).sort! { |a, b| a.created_at <=> b.created_at }
end
this is quite ineffective, as the sorting could be done in sql, but ActiveRecord cannot instantiate records selected from different tables (you can somehow override this by using STI)
Firstly - you can't just write plain SQL in your ruby code and expect it to work.
It's ruby, not SQL. They are different languages.
If you can - use the ruby-way with associations instead (as per the other example).
However - if you desperately need to use raw SQL (eg you have legavy tables that don't match to models or have some complex combination-logic in teh SQL that doesn't easily map to assocations); then you need to pass SQL to the database... which means using a connection via Active Record.
Try:
def feed
ActiveRecord::Base.connection.execute("SELECT * FROM discussions, exchanges GROUP BY created_at")
end
It will not return ruby models for you - just a raw results-object.
I'd recommend trying this in script/console and then doing a "puts my_user.feed.inspect" to have a look at the kind of thing it returns so you know how to use it.
Note: the presence of this kind of thing is considered a strong code smell - only use it where you really need it

not on a query in RoR

In Ruby on rails 3 I want to query on a has_many field of a model as follows:
#project.items.where(:status => 1)
The problem is I'm trying to get the exact opposite result than this. What i want is all items of #project where the status is not 1. Been looking for the answer to this for a while, anyone?
There are many ways to accomplish what you are trying to do, however, some are better than others. If you will always be searching for a hardcoded number (i.e. 1 in this case), then the following solution will work:
#project.items.where('status != 1')
However, if this value is not hard-coded, you are openly vulnerable to SQL injection as Rails will not (cannot) escape this kind of query. As a result, it is preferred among Rails developers to user the following syntax for most custom conditions (those that can't be constructed via Hash):
#project.items.where(['status != ?', 1])
This syntax is slightly confusing, so let me go over it. Basically you are providing the where clause an Array of values. The first value in the array is a String representing the query you want executed. Anywhere you want a value in that string, you place a ?. This serves as a placeholder. Next, you add an element for every question mark in you query. For example, if I had the following:
where(['first_name = ? AND last_name = ?', params[:first_name], params[:last_name]]
Rails will automatically match these up forming the query for you. In that process, it also escapes potentially unsafe characters, preventing injection.
In general, it is preferred to use the Array syntax, even for a hardcoded value. I've been told that pure string conditions in Rails 3.5 will raise a warning (unverified), so it doesn't hurt to get in the process of using the Array syntax now.