activerecord find conditions for associated models - ruby-on-rails

so i havent found much documentation about using conditions on activerecord find methods for associated models, but i have found a variety of examples. although none of them seem to be working for me.
user has_one avatar
avatar belongs_to user
Avatar.find(:all, :include => :user, :conditions => {:user => {:login => 'admin'}})
returns the ridiculously long error
SQLite3::SQLException: no such column: user.login: SELECT "avatars"."id" AS t0_r0, "avatars"."user_id" AS t0_r1, "avatars"."featured" AS t0_r2, "avatars"."avatar_file_name" AS t0_r3, "avatars"."avatar_content_type" AS t0_r4, "avatars"."avatar_file_size" AS t0_r5, "avatars"."created_at" AS t0_r6, "avatars"."updated_at" AS t0_r7, "users"."id" AS t1_r0, "users"."login" AS t1_r1, "users"."email" AS t1_r2, "users"."crypted_password" AS t1_r3, "users"."password_salt" AS t1_r4, "users"."persistence_token" AS t1_r5, "users"."perishable_token" AS t1_r6, "users"."login_count" AS t1_r7, "users"."failed_login_count" AS t1_r8, "users"."last_request_at" AS t1_r9, "users"."current_login_at" AS t1_r10, "users"."last_login_at" AS t1_r11, "users"."current_login_ip" AS t1_r12, "users"."last_login_ip" AS t1_r13, "users"."created_at" AS t1_r14, "users"."updated_at" AS t1_r15 FROM "avatars" LEFT OUTER JOIN "users" ON "users".id = "avatars".user_id WHERE ("user"."login" = 'admin')
i have tried a variety of other patterns including
:conditions => "user.login == 'admin'"
but nothing is working.
this is with a rails 2.3.8 app, but rails3 is installed as well. so i have activerecord 3.0.0 & 2.3.8 installed. i thought this may be an issue, but it seems unlikely.

instead of
:user => {:login => 'admin'}
try
:users => {:login => 'admin'}
users

The standard Rails format for table names in the database itself is "all lower case, plural."
So you can debug your SQL error and see:
no such column: user.login
Since we know the table name is "users" the problem is that you're referencing table "user"
Fix depends on the version of Active Record. I prefer old school since it makes it more clear that the condition clause is SQL, not Ruby/Rails:
Avatar.find(:all, :include => :user,
:conditions => "users.login = 'admin'")
Note: Rails 3 is different from the above...
Added:
Remember that if you're looking up a login supplied as a parameter from the user, you must properly escape the parameter. Rails makes this easy:
user_login = "admin" # or, for example, params['user']
Avatar.find(:all, :include => :user,
:conditions => ["users.login = ?", user_login])

Related

Why isn't my nested association loading?

I have the following models: Mainline -> Release -> Overlay. I'm trying to get all the overlays for a mainline in one eager-loaded query.
(This is a database that was initially started using Entity Framework on SQL Server, so I have the added complication that the keys are not Rails-standard.)
mainline.rb
class Mainline < ActiveRecord::Base
self.primary_key = 'MainlineID'
has_many :releases, foreign_key: 'Mainline_MainlineID'
has_many :overlays, through: :releases
end
release.rb
class Release < ActiveRecord::Base
self.primary_key = 'ReleaseID'
belongs_to :mainline, foreign_key: 'Mainline_MainlineID'
has_many :overlays, foreign_key: 'Release_ReleaseID'
end
overlay.rb
class Overlay < ActiveRecord::Base
self.primary_key = 'OverlayID'
belongs_to :release, foreign_key: 'Release_ReleaseID'
end
I can do the following in the Rails console:
m = Mainline.where(MainlineID: 2025).includes(releases: :overlays).where.not(overlays: { Version: nil }).first
Which gives me the following SQL, which is correct. I can run this in SQL Server Management Studio and get the correct results.
SQL (40.8ms) EXEC sp_executesql N'SELECT DISTINCT TOP (1) [mainlines].[MainlineID] FROM [mainlines] LEFT OUTER JOIN [releases] ON [releases].[Mainline_MainlineID] = [mainlines].[MainlineID] LEFT OUTER JOIN [overlays] ON [overlays].[Release_ReleaseID] = [releases].[ReleaseID] WHERE [mainlines].[MainlineID] = 2025 AND ([overlays].[Version] IS NOT NULL) ORDER BY [mainlines].[MainlineID] ASC'
SQL (138.0ms) EXEC sp_executesql N'SELECT [mainlines].[MainlineID] AS t0_r0, [mainlines].[Program_ProgramID] AS t0_r1, [mainlines].[Designation] AS t0_r2, [mainlines].[Lifecycle] AS t0_r3, [releases].[ReleaseID] AS t1_r0, [releases].[Mainline_MainlineID] AS t1_r1, [releases].[SoftwareVersion] AS t1_r2, [releases].[ModuleName] AS t1_r3, [releases].[FirstProductConfigFileVersion] AS t1_r4, [releases].[ProductIdString] AS t1_r5, [releases].[ModulePartNumber] AS t1_r6, [releases].[InterfaceLevel] AS t1_r7, [releases].[CreationDate] AS t1_r8, [releases].[StartBootLoaderVersion] AS t1_r9, [releases].[EndBootLoaderVersion] AS t1_r10, [releases].[ByteOrder] AS t1_r11, [releases].[IndexTableAddress] AS t1_r12, [releases].[Description] AS t1_r13, [releases].[FileNameBase] AS t1_r14, [releases].[ImportedDate] AS t1_r15, [overlays].[OverlayID] AS t2_r0, [overlays].[Release_ReleaseID] AS t2_r1, [overlays].[Version] AS t2_r2, [overlays].[OverlayDate] AS t2_r3, [overlays].[Proposal_ProposalID] AS t2_r4, [overlays].[Lifecycle] AS t2_r5, [overlays].[Description] AS t2_r6, [overlays].[Comments] AS t2_r7, [overlays].[ImportedDate] AS t2_r8, [overlays].[Proposer] AS t2_r9 FROM [mainlines] LEFT OUTER JOIN [releases] ON [releases].[Mainline_MainlineID] = [mainlines].[MainlineID] LEFT OUTER JOIN [overlays] ON [overlays].[Release_ReleaseID] = [releases].[ReleaseID] WHERE [mainlines].[MainlineID] = 2025 AND ([overlays].[Version] IS NOT NULL) AND [mainlines].[MainlineID] IN (2025) ORDER BY [mainlines].[MainlineID] ASC
My problem is that I can loop over the releases without hitting the database again...
2.2.3 :137 > m.releases
=> #<ActiveRecord::Associations::CollectionProxy [#<Release ReleaseID: 126, Mainline_MainlineID: 2025, SoftwareVersion: "40190004_0", ModuleName: "CM23xx", FirstProductConfigFileVersion: "2003.1.1.0", ProductIdString: "BHQ", ModulePartNumber: "9999999", InterfaceLevel: "4.6.0.0", CreationDate: "2015-06-16 00:0
But not the overlays...
2.2.3 :138 > m.overlays
Overlay Load (78.8ms) EXEC sp_executesql N'SELECT [overlays].* FROM [overlays] INNER JOIN [releases] ON [overlays].[Release_ReleaseID] = [releases].[ReleaseID] WHERE [releases].[Mainline_MainlineID] = #0', N'#0 int', #0 = 2025 [["Mainline_MainlineID", 2025]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Overlay OverlayID: 3747, Release_ReleaseID: 126, Version: 86, OverlayDate: "2015-07-30 00:00:00", Proposal_ProposalID: nil, Lifecycle: "Imported", Description: nil, Comments: nil, ImportedDate: "2015-08-24 13:59:44", Proposer: nil>, #<Overl
It's generating another query, this time without my condition that Version is not NULL. Why isn't Rails recognizing that I already have the overlays I want included my selected mainline?
UPDATE: Right after posting initially, I tried the following:
m = Mainline.includes(:releases, :overlays).where.not(overlays: { Version: nil }).find(2025)
Note the change in the .includes(). This gave me what I wanted to see in the console:
2.2.3 :066 > m.overlays
=> #<ActiveRecord::Associations::CollectionProxy [#<Overlay OverlayID: 3747, Release_ReleaseID: 126, Version: 86, OverlayDate: "2015-07-30 00:00:00", Proposal_ProposalID: nil, Lifecycle: "Imported", Description: nil, Comments: nil, ImportedDate: "2015-08-24 13:59:44", Proposer: n
This generates much different SQL:
SQL (657.4ms) EXEC sp_executesql N'SELECT [mainlines].[MainlineID] AS t0_r0, [mainlines].[Program_ProgramID] AS t0_r1, [mainlines].[Designation] AS t0_r2, [mainlines].[Lifecycle] AS t0_r3, [releases].[ReleaseID] AS t1_r0, [releases].[Mainline_MainlineID] AS t1_r1, [releases].[SoftwareVersion] AS t1_r2, [releases].[ModuleName] AS t1_r3, [releases].[FirstProductConfigFileVersion] AS t1_r4, [releases].[ProductIdString] AS t1_r5, [releases].[ModulePartNumber] AS t1_r6, [releases].[InterfaceLevel] AS t1_r7, [releases].[CreationDate] AS t1_r8, [releases].[StartBootLoaderVersion] AS t1_r9, [releases].[EndBootLoaderVersion] AS t1_r10, [releases].[ByteOrder] AS t1_r11, [releases].[IndexTableAddress] AS t1_r12, [releases].[Description] AS t1_r13, [releases].[FileNameBase] AS t1_r14, [releases].[ImportedDate] AS t1_r15, [overlays].[OverlayID] AS t2_r0, [overlays].[Release_ReleaseID] AS t2_r1, [overlays].[Version] AS t2_r2, [overlays].[OverlayDate] AS t2_r3, [overlays].[Proposal_ProposalID] AS t2_r4, [overlays].[Lifecycle] AS t2_r5, [overlays].[Description] AS t2_r6, [overlays].[Comments] AS t2_r7, [overlays].[ImportedDate] AS t2_r8, [overlays].[Proposer] AS t2_r9 FROM [mainlines] LEFT OUTER JOIN [releases] ON [releases].[Mainline_MainlineID] = [mainlines].[MainlineID] LEFT OUTER JOIN [releases] [releases_mainlines_join] ON [releases_mainlines_join].[Mainline_MainlineID] = [mainlines].[MainlineID] LEFT OUTER JOIN [overlays] ON [overlays].[Release_ReleaseID] = [releases_mainlines_join].[ReleaseID] WHERE ([overlays].[Version] IS NOT NULL) AND [mainlines].[MainlineID] = #0 AND [mainlines].[MainlineID] IN (2025)', N'#0 int', #0 = 2025 [["MainlineID", 2025]]
So now I have 2 problems. First, I don't understand why that .includes() syntax works, when everything I've read on the subject makes me think it really ought to be (releases: :overlays), and NOT comma-separated.
Second, and more importantly, my goal is to get 2 more associations to pre-load, and they get hairier. If I can't understand this first example, there's no way I can get the others loaded in. I eventually need to get to the following:
Mainline -> Release -> Overlay -> Calibration <- Parameter
How would I load all of this data with .includes() in the correct hash/array arrangement? From what I've read, I'd expect .includes(release: [overlay: [calibration: :parameter]]), but nothing I've tried gets me even close. Just attempts to try to add the calibration level get me cross products of the entire database.
And that's just the core set of data I need to get out of a single query. I also need to combine users and ownerships and categories, etc. My core "mainline" has about 17K lines, with about 8 different models involved. I absolutely cannot afford any sort of N+1 query with this application. I must get it all in one go. (I've successfully set it up as a find_by_sql(), which gets me precisely the data I need, but I'd like to get it going "the Rails way," so that I can traverse the relationships as expected.)
I've tried playing with .joins(), since that does INNER JOINS, which is what I really need, but it doesn't seem to pre-load the data. I've even tried combining .joins() AND .includes() as I've seen some people do, but now I'm just guessing. I don't understand why what I'm trying, in this first pass, doesn't work, and, until I do, I'm just wasting time.
I see now that I've misunderstood how to reference eager-loaded data. Despite the proper setup with has_many through:, you can't skip over a second association and directly access the third inside the eager-loaded data. I'll never be able to reference members of #mainline.overlays without incurring another query. I have to reference them as #mainline.releases[0].overlays[0].
The relieving news is that I wasn't insane about the syntax of the includes. I was just using the result incorrectly. But, based on this way of accessing my data, I've changed the query around to make what I'm really interested in (calibrations) the top-level query, like so:
cals = Calibration
.joins(overlay: [release: :mainline])
.where(mainlines: { MainlineID: params[:id] })
.where.not(overlays: { Version: nil })
.includes(:parameter, overlay: [release: :mainline])
Then I can loop over cals and access the parameter data, which is my first step/concern.
I also see now the role of .joins() in limiting my data set based on associated values. My current (limited) result set takes 3 seconds to do this way, compared with my .find_by_sql() method, which only takes about half of that, and already includes a lot more models I have yet to include, so I don't know if this is going to work very well, but at least I understand what's going on now.
(Of course, both methods have the problem that, once ~20K lines of data are handed off the browser, the browser takes 10 seconds to generate the table, but that's a horse of another color.)

For the sake of efficiency and optimization, should I use eager_load or includes?

Currently, my code reads like this:
current_user.association.includes(a: [:b, {c: :d}, {e: :f}]).to_a
When doing a call, it seems every single includes is called through its own SELECT call to the DB.
However, when I do current_user.association.eager_load(a: [:b, {c: :d}, {e: :f}]).to_a I see one huge SELECT call.
I ask because I haven't seen this raised before. I would assume that the eager_load is more efficient due to less DB calls.
As I can't infer the query from your description (a: [:b, {c: :d}, {e: :f}]), I need to talk about includes for a little bit.
includes is a query method which accommodates in different situations.
Here are some example code:
# model and reference
class Blog < ActiveRecord::Base
has_many :posts
# t.string "name"
# t.string "author"
end
class Post < ActiveRecord::Base
belongs_to :blog
# t.string "title"
end
# seed
(1..3).each do |b_id|
blog = Blog.create(name: "Blog #{b_id}", author: 'someone')
(1..5).each { |p_id| blog.posts.create(title: "Post #{b_id}-#{p_id}") }
end
In one case, it fires two separate queries, just like preload.
> Blog.includes(:posts)
Blog Load (2.8ms) SELECT "blogs".* FROM "blogs"
Post Load (0.7ms) SELECT "posts".* FROM "posts" WHERE "posts"."blog_id" IN (1, 2, 3)
In another case, when querying on the referenced table, it fires only one LEFT OUTER JOIN query, just like eager_load.
> Blog.includes(:posts).where(posts: {title: 'Post 1-1'})
SQL (0.3ms) SELECT "blogs"."id" AS t0_r0, "blogs"."name" AS t0_r1, "blogs"."author" AS t0_r2, "blogs"."created_at" AS t0_r3, "blogs"."updated_at" AS t0_r4, "posts"."id" AS t1_r0, "posts"."title" AS t1_r1, "posts"."created_at" AS t1_r2, "posts"."updated_at" AS t1_r3, "posts"."blog_id" AS t1_r4 FROM "blogs" LEFT OUTER JOIN "posts" ON "posts"."blog_id" = "blogs"."id" WHERE "posts"."title" = ? [["title", "Post 1-1"]]
So, I think you may asking for the different part of includes and eager_load, which is
Should we use two separate queries or one LEFT OUTER JOIN query for the sake of efficiency and optimisation?
This also confuses me. After some digging, I've found this article by Fabio Akita convinced me. Here are some references and example:
For some situations, the monster outer join becomes slower than many smaller queries. The bottom line is: generally it seems better to split a monster join into smaller ones. This avoid the cartesian product overload problem.
The longer and more complex the result set, the more this matters because the more objects Rails would have to deal with. Allocating and deallocating several hundreds or thousands of small duplicated objects is never a good deal.
Example for query data from Rails
> Blog.eager_load(:posts).map(&:name).count
SQL (0.9ms) SELECT "blogs"."id" AS t0_r0, "blogs"."name" AS t0_r1, "blogs"."author" AS t0_r2, "blogs"."created_at" AS t0_r3, "blogs"."updated_at" AS t0_r4, "posts"."id" AS t1_r0, "posts"."title" AS t1_r1, "posts"."created_at" AS t1_r2, "posts"."updated_at" AS t1_r3, "posts"."blog_id" AS t1_r4 FROM "blogs" LEFT OUTER JOIN "posts" ON "posts"."blog_id" = "blogs"."id"
=> 3
Example for SQL data returned from LEFT OUTER JOIN query
sqlite> SELECT "blogs"."id" AS t0_r0, "blogs"."name" AS t0_r1, "blogs"."author" AS t0_r2, "blogs"."created_at" AS t0_r3, "blogs"."updated_at" AS t0_r4, "posts"."id" AS t1_r0, "posts"."title" AS t1_r1, "posts"."created_at" AS t1_r2, "posts"."updated_at" AS t1_r3, "posts"."blog_id" AS t1_r4 FROM "blogs" LEFT OUTER JOIN "posts" ON "posts"."blog_id" = "blogs"."id";
1|Blog 1|someone|2015-11-11 15:22:35.015095|2015-11-11 15:22:35.015095|1|Post 1-1|2015-11-11 15:22:35.053689|2015-11-11 15:22:35.053689|1
1|Blog 1|someone|2015-11-11 15:22:35.015095|2015-11-11 15:22:35.015095|2|Post 1-2|2015-11-11 15:22:35.058113|2015-11-11 15:22:35.058113|1
1|Blog 1|someone|2015-11-11 15:22:35.015095|2015-11-11 15:22:35.015095|3|Post 1-3|2015-11-11 15:22:35.062776|2015-11-11 15:22:35.062776|1
1|Blog 1|someone|2015-11-11 15:22:35.015095|2015-11-11 15:22:35.015095|4|Post 1-4|2015-11-11 15:22:35.065994|2015-11-11 15:22:35.065994|1
1|Blog 1|someone|2015-11-11 15:22:35.015095|2015-11-11 15:22:35.015095|5|Post 1-5|2015-11-11 15:22:35.069632|2015-11-11 15:22:35.069632|1
2|Blog 2|someone|2015-11-11 15:22:35.072871|2015-11-11 15:22:35.072871|6|Post 2-1|2015-11-11 15:22:35.078644|2015-11-11 15:22:35.078644|2
2|Blog 2|someone|2015-11-11 15:22:35.072871|2015-11-11 15:22:35.072871|7|Post 2-2|2015-11-11 15:22:35.081845|2015-11-11 15:22:35.081845|2
2|Blog 2|someone|2015-11-11 15:22:35.072871|2015-11-11 15:22:35.072871|8|Post 2-3|2015-11-11 15:22:35.084888|2015-11-11 15:22:35.084888|2
2|Blog 2|someone|2015-11-11 15:22:35.072871|2015-11-11 15:22:35.072871|9|Post 2-4|2015-11-11 15:22:35.087778|2015-11-11 15:22:35.087778|2
2|Blog 2|someone|2015-11-11 15:22:35.072871|2015-11-11 15:22:35.072871|10|Post 2-5|2015-11-11 15:22:35.090781|2015-11-11 15:22:35.090781|2
3|Blog 3|someone|2015-11-11 15:22:35.093902|2015-11-11 15:22:35.093902|11|Post 3-1|2015-11-11 15:22:35.097479|2015-11-11 15:22:35.097479|3
3|Blog 3|someone|2015-11-11 15:22:35.093902|2015-11-11 15:22:35.093902|12|Post 3-2|2015-11-11 15:22:35.103512|2015-11-11 15:22:35.103512|3
3|Blog 3|someone|2015-11-11 15:22:35.093902|2015-11-11 15:22:35.093902|13|Post 3-3|2015-11-11 15:22:35.108775|2015-11-11 15:22:35.108775|3
3|Blog 3|someone|2015-11-11 15:22:35.093902|2015-11-11 15:22:35.093902|14|Post 3-4|2015-11-11 15:22:35.112654|2015-11-11 15:22:35.112654|3
3|Blog 3|someone|2015-11-11 15:22:35.093902|2015-11-11 15:22:35.093902|15|Post 3-5|2015-11-11 15:22:35.117601|2015-11-11 15:22:35.117601|3
We got the expected result from Rails, but bigger result from SQL. And that's the efficiency lose for the LEFT OUTER JOIN.
So my conclusion is, prefer includes over eager_load.
I've concluded a blog post about Preload, Eager_load, Includes, References, and Joins in Rails while researching. Hope this can help.
Reference
Remove N+1 queries in your Ruby on Rails app
Rails :include vs. :joins
Preload, Eagerload, Includes and Joins
Rolling with Rails 2.1 - The First Full Tutorial - Part 2
So, as it turns out, at one point ActiveRecord actually attempted to get everything into one query, but then opted it wasn't such a good idea.
I explored this with my query above and 4000 records.
A quick analysis:
eager_load took 2,600 milliseconds.
includes took 72 milliseconds.
eager_load took 36 times as long as includes.

searching by email in ActiveRecord using named scope "<field> is ambiguous'

Using rails 3.2.6.
# 1 letter domain name in email without scope
> Member.where('UPPER(email) LIKE UPPER(?)' , "a#b.com")
Member Load (0.7ms) SELECT "members".* FROM "members" WHERE (UPPER(email) LIKE UPPER('a#b.com'))
=> []
# 2 letter domain name in email without scope
> Member.where('UPPER(email) LIKE UPPER(?)' , "a#bc.com")
Member Load (0.7ms) SELECT "members".* FROM "members" WHERE (UPPER(email) LIKE UPPER('a#bc.com'))
=> []
# 1 letter domain name in email with scope
> Member.with_households.where('UPPER(email) LIKE UPPER(?)' , "a#b.com")
Member Load (0.7ms) SELECT "members".* FROM "members" WHERE (UPPER(email) LIKE UPPER('a#b.com'))
=> []
# 2 letter domain name in email with scope
> Member.with_households.where('UPPER(email) LIKE UPPER(?)' , "a#bs.com")
SQL (0.6ms) SELECT "members"."id" AS t0_r0, "members"."last_name" AS t0_r1, "members"."first_name" AS t0_r2, "members"."household_id" AS t0_r3, "members"."created_at" AS t0_r4, "members"."updated_at" AS t0_r5, "members"."phone1" AS t0_r6, "members"."phone2" AS t0_r
7, "members"."address1" AS t0_r8, "members"."address2" AS t0_r9, "members"."city" AS t0_r10, "members"."state" AS t0_r11, "members"."zip" AS t0_r12, "members"."notes" AS t0_r13, "members"."active" AS t0_r14, "members"."email" AS t0_r15, "households"."id" AS t1_r0, "ho
useholds"."balance" AS t1_r1, "households"."created_at" AS t1_r2, "households"."updated_at" AS t1_r3, "households"."notes" AS t1_r4, "members_households"."id" AS t2_r0, "members_households"."last_name" AS t2_r1, "members_households"."first_name" AS t2_r2, "members_hou
seholds"."household_id" AS t2_r3, "members_households"."created_at" AS t2_r4, "members_households"."updated_at" AS t2_r5, "members_households"."phone1" AS t2_r6, "members_households"."phone2" AS t2_r7, "members_households"."address1" AS t2_r8, "members_households"."ad
dress2" AS t2_r9, "members_households"."city" AS t2_r10, "members_households"."state" AS t2_r11, "members_households"."zip" AS t2_r12, "members_households"."notes" AS t2_r13, "members_households"."active" AS t2_r14, "members_households"."email" AS t2_r15 FROM "members
" LEFT OUTER JOIN "households" ON "households"."id" = "members"."household_id" LEFT OUTER JOIN "members" "members_households" ON "members_households"."household_id" = "households"."id" WHERE (UPPER(email) LIKE UPPER('a#bs.com'))
ActiveRecord::StatementInvalid: PGError: ERROR: column reference "email" is ambiguous
LINE 1: ..."."household_id" = "households"."id" WHERE (UPPER(email) LIK...
Here's the error:
ActiveRecord::StatementInvalid: PGError: ERROR: column reference "email" is ambiguous
A Household has_many Members.
Here's the with_households scope definition:
scope :with_households, :include => [{:household => :members}]
The email domain length may be a red herring, but I couldn't reproduce the error otherwise. Why is Arel doing a bunch of joins in just this case?
Try
Member.with_households.where('UPPER(members.email) LIKE UPPER(?)' , "a#bs.com")
ActiveRecord StatementInvalid is ambiguous is ARs way of saying: Mister, your SQL statement does not make sense because a field you are examining is found on several tables and I don't know which to use.
I'm guessing the with_households scope looks at another table which also has an email field. Or maybe you have a default_scope which does the same. Try specifying the table name everywhere, e.g. members.email and managers.email.
Member.with_households.where(Member.arel_table[:email].matches("a#bs.com"))

ActiveRecord returns model objects with un-initialized associations

I have a simple join query which in some cases returns ActiveRecord objects with uninitialized associations, and I try to understand why. (My setup: rails 2.3.8 with MySQL)
Here are my models:
class Member
has_many :twitter_status_relations
//has some more unrelated associations
end
class TwitterStatus
has_many :twitter_status_relations
end
class TwitterStatusRelation
belongs_to :member
belongs_to :twitter_status
end
And here is the query I perform:
result = TwitterStatusRelation.all(:joins => :twitter_status,
:conditions=>{:twitter_statuses=>{:sent_at=>1.month.ago..DateTime.now}}, :include=>:member,:group=>"twitter_status_relations.member_id")
Now, when I run in it the first time in the app, it works fine:
print result[0].member, result[0].member.class.reflect_on_all_associations(:has_many)
#=> <Member...>, [<ActiveRecord::Reflection::AssociationReflection,...]
BUT, when I run it again, and try accessing any association of the member, I get nil exception. Print shows the following:
print result[0].member, result[0].member.class.reflect_on_all_associations(:has_many)
#=> <Member...>, [-- empty ---]
Looks like the member object doesn't have any associations, and so when I try to access any of it, I get an exception.
Do you have any idea why ActiveRecord wouldn't initialize associations of the returned objects in some cases? I would appreciate any half-idea because I'm stuck.
Here is the SQL the above query produces (Ran posted the question on my behalf). The SQL has more fields then the query, because I simplified the query when posting it, removing conditions that aren't relevant to the problem.
SELECT `twitter_status_relations`.`id` AS t0_r0,
twitter_status_relations.twitter_status_id AS t0_r1,
twitter_status_relations.source_twitter_identity_id AS t0_r2,
twitter_status_relations.relation_type AS t0_r3,
twitter_status_relations.relation_data AS t0_r4,
twitter_status_relations.linked_twitter_identity_id AS t0_r5,
twitter_status_relations.user_id AS t0_r6,
twitter_status_relations.linked_member_id AS t0_r7, members.id
AS t1_r0, members.user_id AS t1_r1, members.name AS t1_r2,
members.email AS t1_r3, members.member_rating AS t1_r4,
members.created_at AS t1_r5, members.updated_at AS t1_r6,
members.merged_with_member_id AS t1_r7, members.engage_rating
AS t1_r8, members.support_rating AS t1_r9,
members.user_engage_rating AS t1_r10,
members.user_support_rating AS t1_r11,
members.influence_rating AS t1_r12, members.twitter_username
AS t1_r13, members.lead_rating AS t1_r14,
members.follow_rating AS t1_r15, members.unfollow_rating AS
t1_r16, members.followers_count AS t1_r17, members.hidden AS
t1_r18 FROM twitter_status_relations LEFT OUTER JOIN members ON
members.id = twitter_status_relations.linked_member_id WHERE
(twitter_status_relations.user_id = 1 AND
twitter_status_relations.relation_type IN(
'mention','reply','received_dm','retweet','link','term','hashtag' )
AND twitter_status_relations.linked_member_id IN(
83995,128457,21421,138316,128455,97475,128453,436231,82236,441208,138564,138337,436223,436222,441093,21194,441088,441092,438998,442752,138331,138327,138325,444897,9277,12,509521,13,15,534511,7606,7447,200,7,4,17200,5,652302,1,5536,18770,652301,214082,150870,436228,81204,436225,662513,138608,138338
)) AND twitter_status_relations.id IN (8304, 26493, 113492, 113638,
1, 6, 41213, 113493, 20, 26173) GROUP BY
twitter_status_relations.linked_member_id ORDER BY
members.member_rating

Rails, scope, OR and joins

I have a scope:
includes(:countries).where("profiles.sector = :sector OR advices.sector = :sector", :sector => sector)
It produces the following SQL:
SELECT `profiles`.* FROM `profiles` INNER JOIN `advices` ON `advices`.`profile_id` = `profiles`.`id` WHERE (profiles.sector = 'Forestry_paper' OR advices.sector = 'Forestry_paper')
(yes I have country in my Profile and in my Country model)
Unfortunately, the OR seems to fail:
it doesn't render a profile having only the proper sector but no related advice. Thoughts?
You are doing an INNER JOIN, so it requires that the profiles have a corresponding advice. Try the following instead:
Profile
.joins("LEFT JOIN advices ON advices.profile_id = profiles.id")
.where("profiles.sector = :sector OR advices.sector = :sector", :sector => sector)
This will also include profiles that have no advices.
You can do outer joins by specifying a where clause with a hash after the includes:
Post.includes(:comments).where(:comments=>{:user_id=>nil})
produces:
Post Load (0.5ms) SELECT "posts"."id" AS t0_r0, "posts"."created_at" AS t0_r1,
"posts"."updated_at" AS t0_r2, "comments"."id" AS t1_r0, "comments"."user_id"
AS t1_r1, "comments"."post_id" AS t1_r2, "comments"."content" AS t1_r3,
"comments"."created_at" AS t1_r4, "comments"."updated_at" AS t1_r5
FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
WHERE ("comments"."user_id" IS NULL)
Ryan Bigg wrote a helpful blog post about this.
EDIT
Be aware that this technique is more or less a side effect of the way Rails constructs the SQL for eager-loading associations. An explicit LEFT JOIN is more robust, as suggested in the accepted answer.
Check out http://metautonomo.us/projects/metawhere/ for more query goodness...
meta_where is now unmaintained: https://github.com/activerecord-hackery/meta_where
Rails 5 is introducing OR statements:
Rails 5: ActiveRecord OR query

Resources