Self join in ruby on rails - ruby-on-rails-3

I am having trouble with a self join statement in Ruby on Rails.
Well, it is kind of complicated and irritating but what I want to get is:
5 jobs out of the JobCompact table that has the same foreign key build_compact_id and either the language "ruby" or "rubinius"
My query looks as follows:
JobCompact.all(
:joins => "JOIN job_compacts AS jobs2 ON job_compacts.build_compact_id = jobs2.build_compact_id",
:conditions => ["job_compacts.language = ? AND jobs2.language=?", 'ruby', 'rubinius'],
:limit => 5)
In general that seems to be working but only return me objects out of the first table. The second table (jobs2) is not shown in the result set. I would like to get a collection with all job_compacts that match the given condition.
I hope I could explain my problem, otherwise do not hesitate to ask me. Thanks in advance!

Related

Rails eager loading and conditions

I have the following associations set up
class bookinghdr
belongs_to :agent
end
class bookingitem
belongs_to :bookinghdr, :include => agent
end
So I was expecting to be able to do the following:
named_scope :prepay, :include=>["bookinghdr"], :conditions => ["bookinghdr.agent.agenttype = 'PP'"]
and in my controller do:
b = Bookingitem.prepay
But that gives me a ActiveRecord::StatementInvalid: Mysql::Error: Unknown column 'bookinghdr.agent.agenttype'
However if I don't include the conditions clause then I get a recordset on which I can do:
b = Bookingitem.prepay
b[0].bookinghdr.agent.agenttype
without any error!
I don't want to have to get all the records and then iterate over them to find the ones whose agent has a 'PP# flag. I was hoping that AR would do that for me.
Anybody got any ideas on how to achieve this?
Your question shows that you have not yet fully understood how associations and named scopes work. Since I cannot tell from your question what parts aren't clear, I suggest you read the Association Basics guide at http://guides.rubyonrails.org/v2.3.11/association_basics.html. This should bring you up to speed regarding the concepts you want to implement. After you have read the guide it should all make sense.

Rails 3: ActiveRecord query with :includes only returns results that have related values in included table?

I've got a model (a Feature) that can have many Assets. These Assets each have an issue_date. I'm struggling with what seems like a simple ActiveRecord query to find all Features and their Assets with an issue_date of tomorrow, regardless of if there are Assets or not — preferably with one query.
Here's my query right now.
Feature.includes(:assets).where(:assets => { :issue_date => Date.tomorrow })
Unfortunately, this returns only the Features that have Assets with an issue_date of tomorrow. Even stranger, the generated SQL looks like this (tomorrow's obviously the 19th).
SELECT `features`.* FROM `features` WHERE `assets`.`issue_date` = '2011-08-19'
Shouldn't this have an LEFT JOIN in there somewhere? That's the sort of thing I'm going for. Using joins instead of includes does an INNER JOIN, but that's not what I want. Strangely enough, it seems like I'm getting an INNER JOIN-type of behavior. When I run that includes query above, the actual SQL that's spit out looks something like this...
SELECT `features`.`id` AS t0_r0, `features`.`property_id` AS t0_r1,
// every other column from features truncated for sanity
`assets`.`feature_id` AS t1_r1, `assets`.`asset_type` AS t1_r2,
// all other asset columns truncated for sanity
FROM `features`
LEFT OUTER JOIN `assets` ON `assets`.`feature_id` = `features`.`id`
WHERE `assets`.`issue_date` = '2011-08-19'
Which looks like it should work right but it doesn't. I get only the Features that have Assets with an issue_date of tomorrow. Any idea what I'm doing wrong?
I've tried the older, Rails v2 way of doing it…
Feature.find(:all,
:include => :assets,
:conditions => ['assets.issue_date = ?', Date.tomorrow])
Which gives me the same results. There's one Feature I know that doesn't have any Assets for tomorrow, and it's not in that list.
I've also poked around and found similar questions, but I couldn't seem to find one that explained this opposite behavior I'm seeing.
Edit: I'm so close. This gets me all the Feature objects.
Feature.joins("LEFT OUTER JOIN assets on assets.feature_id = feature.id AND asset.issue_date = #{Date.tomorrow}")
It does not, however, get me the matching Assets bundled into the object. With feature as a returned item in the query, feature.assets makes another call to the database, which I don't want. I want feature.assets to return only those I've specified in that LEFT OUTER JOIN call. What else do I need to do to my query?
I thought this would get me what I needed, but it doesn't. Calling feature.assets (with feature as an item returned in my query) does another query to look for all assets related to that feature.
Feature.joins("LEFT OUTER JOIN assets on assets.feature_id = feature.id AND asset.issue_date = #{Date.tomorrow}")
So here's what does work. Seems a little cleaner, too. My Feature model already has a has_many :assets set on it. I've set up another association with has_many :tomorrows_assets that points to Assets, but with a condition on it. Then, when I ask for Feature.all or Feature.name_of_scope, I can specify .includes(:tomorrows_assets). Winner winner, chicken dinner.
has_many :tomorrows_assets,
:class_name => "Asset",
:readonly => true,
:conditions => "issue_date = '#{Date.tomorrow.to_s}'"
I can successfully query Features and get just what I need included with it, only if it matches the specified criteria (and I've set :readonly because I know I'll never want to edit Assets like this). Here's an IRB session that shows the magic.
features = Feature.includes(:tomorrows_assets)
feature1 = features.find_all{ |f| f.name == 'This Feature Has Assets' }.first
feature1.tomorrows_assets
=> [#<Asset id:1>, #<Asset id:2>]
feature2 = features.find_all{ |f| f.name == 'This One Does Not' }.first
feature2.tomorrows_assets
=> []
And all in only two SQL queries.
I had a very similar problem and managed to solve it using the following query;
Feature.includes(:assets).where('asset.id IS NULL OR asset.issue_date = ?', Date.tomorrow)
This will load all features, regardless of whether it has any assets. Calling feature.asset will return an array of assets if available without running another query
Hope that helps someone!
You have to specify the SQL for outer joins yourself, the joins method only uses inner joins.
Feature.joins("LEFT OUTER JOIN assets ON assets.feature_id = features.id").
where(:assets => {:issue_date => Date.tomorrow})
Have you tried:
Feature.joins( :assets ).where( :issue_date => Date.tomorrow );
The guide here suggests the includes method is used to reduce the number of queries on a secondary table, rather than to join the two tables in the way you're attempting.
http://guides.rubyonrails.org/active_record_querying.html

Rails ActiveRecord Find with Date

I'm having a hard time figuring this out but how do I tell my finder statement to ignore the time of the Datetime field in the db?
def trips_leaving_in_two_weeks
Trip.find(:all, :conditions => ["depart_date = ?", 2.weeks.from_now.to_date])
end
I want depart_date to come back as just a date but it keeps returning the time as well and causing this equality not to work. Is there someway to just compare against the dates? Thanks
Edit
Here's the code I'm using now that works:
Trip.find(:all, :conditions => ["DATE(depart_date) = ?", 2.weeks.from_now.to_date])
Not sure which DB you're using but does this work?
"depart_date = DATE(?)"
I would use this approach:
Rails 3.x
Trip.where(
:depart_date => 2.weeks.from_now.beginning_of_day..2.weeks.from_now.end_of_day
)
Rails 2.x
Trip.all(
:conditions => {
:depart_date => 2.weeks.from_now.beginning_of_day..2.weeks.from_now.end_of_day
})
If you index the depart_date column this solution will be efficient as the query uses the index. This solution is DB neutral.
When calculated fields are used in a where clause, the performance degrades(unless there is a special index).

How can I find records whose :name do NOT equal an array provided by another query?

Currently my users can add locations to their profiles via a form which includes this statement: (I'm using RoR3, HAML, sqlite3 for dev, and mysql for prod)
= select_tag "id", options_from_collection_for_select(Location.all, 'id', 'name')
However, this allows the user to add the same location multiple times. I would like to list only the locations which the user has NOT already posted. So I would like to do something like:
Location.find(:all, :conditions => ["name != ?", user.locations])
This of course does not work whereas this does.
Location.find(:all, :conditions => ["name != ?", "New York"])
That's because user.locations returns an array. I haven't the slightest idea how to proceed at this point. Other than learning SQL I suppose. Is there a method for this that I'm not finding?
Something like:
Location.find(:all, :conditions => ["name not in (?)", user.locations])
should do it (although admittedly less efficient than doing an outer join and filtering null user_ids ) depending on what your array of "user.locations" actually are.
As a side note, learning SQL will make you a much more capable (and marketable) web-developer ... food for thought.

My Rails queries are starting to get complicated, should I switch to raw SQL queries? What do you do?

My Rails app is starting to need complicated queries. Should I just start using raw SQL queries? What is the trend in the Rails community?
Update:
I do not have written queries right now, I wanted to ask this question before I start. But here is an example of what I want to do:
I have books which have categories. I want to say-
Give me all books that were:
-created_at (added to store) between date1 and date2
-updated_at before date3
-joined with books that exist in shopping carts right now
I haven't written the query yet but I think the rails version will be something like this:
books_to_consider = Book.find(:all,
:conditions => "created_at <= '#{date2}' AND created_at >= '#{date1}' AND updated_at <= '#{date3}'",
:joins => "as b inner join carts as c on c.book_id = b.id")
I am not saying ActiveRecord can't handle this query, but is it more accepted to go with raw SQL for readability (or maybe there are other limitations I don't know of yet)?
The general idea is to stick to ActiveRecord-generated queries as much as possible, and use SQL fragments only where necessary. SQL fragments are explicitly supported because the creators of ActiveRecord realised that SQL cannot be completely abstracted away.
Using the the find method without SQL fragments is generally rewarded with better maintainability. Given your example, try:
Book.find(:all,
:conditions => ["created_at >= ? AND created_at <= ? AND updated_at <= ?",
date1, date2, date3]
:include => :carts)
The :inlude => :carts will do the join if you added has_many :carts to your Book model. As you can see, there does not have to be much SQL involved. Even the quoting and escaping of input can be left to Rails, while still using SQL literals to handle the >= and <= operators.
Going a little bit further, you can make it even clearer:
class Book < AciveRecord::Base
# Somewhere in your Book model:
named_scope :created_between, lambda { |start_date, end_date|
{ :conditions => { :created_at => start_date..end_date } }
}
named_scope :updated_before, lambda { |date|
{ :conditions => ["updated_at <= ?", date] }
}
# ...
end
Book.created_between(date1, date2).updated_before(date3).find(:all,
:include => :carts)
Update: the point of the named_scopes is, of course, to reuse the conditions. It's up to you to decide whether or not it makes sense to put a set of conditions in a named scope or not.
Like molf is saying with :include, .find() has the advantage of eager loading of children.
Also, there are several plugins, like pagination, that will wrap the find function. You'll have to use .find() to use the plugins.
If you have a really complex sql query remember that .find() uses your exact parameter string. You can always inject your own sql code:
:conditions => ["id in union (select * from table...
And don't forget there are a lot of optional parameters for .find()
:conditions - An SQL fragment like "administrator = 1", [ "user_name = ?", username ], or ["user_name = :user_name", { :user_name => user_name }]. See conditions in the intro.
:order - An SQL fragment like "created_at DESC, name".
:group - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
:having - Combined with +:group+ this can be used to filter the records that a GROUP BY returns. Uses the HAVING SQL-clause.
:limit - An integer determining the limit on the number of rows that should be returned.
:offset - An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4.
:joins - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), named associations in the same form used for the :include option, which will perform an INNER JOIN on the associated table(s), or an array containing a mixture of both strings and named associations. If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table‘s columns. Pass :readonly => false to override.
:include - Names associations that should be loaded alongside. The symbols named refer to already defined associations. See eager loading under Associations.
:select - By default, this is "*" as in "SELECT * FROM", but can be changed if you, for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name").
:from - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name of a database view).
:readonly - Mark the returned records read-only so they cannot be saved or updated.
:lock - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". :lock => true gives connection‘s default exclusive lock, usually "FOR UPDATE".
src: http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002553