Rails includes doesn't load desired data - ruby-on-rails

I have two simple models, ProjectSetting and ProjectSettingQuestions. ProjectSettingQuestions belongs to ProjectSetting. I want to load ProjectSettingQuestions data when I query ProjectSetting. Can't seem to possibly do that with this query:
ProjectSetting.includes(:project_setting_questions).where(:project_id=>params[:project_id])
params is not the issue. This line gets ProjectSetting data but not the questions. Thanks!
Logs show the following:
ProjectSetting Load (0.3ms) SELECT "project_settings".* FROM "project_settings" WHERE "project_settings"."project_id" = 31 LIMIT 1
ProjectSettingQuestion Load (0.4ms) SELECT "project_setting_questions".* FROM "project_setting_questions" WHERE "project_setting_questions"."project_setting_id" IN (2)

Assuming you are having has_many and belongs_to relationship, you can do it like this:
project_setting = ProjectSetting.first
project_setting.project_setting_questions
Or you can also do it this way, since ProjectSettingQuestions belongs_to ProjectSetting, it contains the foreign_key of ProjectSetting
project_setting = ProjectSetting.first
ProjectSettingQuestion.where(:project_setting_id => project_setting.id)
You should take a look at this guide if you haven't already:
http://guides.rubyonrails.org/association_basics.html
Hope it helps.

Related

Includes still result in second database query when using relation with limited columns

I'm trying to use includes on a query to limit the number of subsequent database calls that fire when rendering but I also want the include calls to select a subset of columns from the related tables. Specifically, I want to get a set of posts, their comments, and just the name of the user who wrote each comment.
So I added
belongs_to :user
belongs_to :user_for_display, :select => "users.id, user.name", :class_name => "User", :foreign_key => "user_id"
to my comments model.
From the console, when I do
p = Post.where(:id => 1).includes(comments: [:user_for_display])
I see that the correct queries fire:
SELECT posts.* FROM posts WHERE posts.id = 1
SELECT comments.* FROM comments comments.attachable_type = "Post" AND comments.attachable_id IN (1)
SELECT users.id, users.name FROM users WHERE users.id IN (1,2,3)
but calling
p.first.comments.first.user.name
still results in a full user load database call:
User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 11805 LIMIT 1
=> "John"
Referencing just p.first.comments does not fire a second comments query. And if I include the full :user relation instead of :user_for_display, the call to get the user name doesn't fire a second users query (but i'd prefer not to be loading the full user record).
Is there anyway to use SELECT to limit fields in an includes?
You need to query with user_for_display instead of user.
p.first.comments.first.user_for_display.name

Rails 4 Eager load limit subquery

Is there a way to avoid the n+1 problem when eager loading and also applying a limit to the subquery?
I want to avoid lots of sql queries like this:
Category.all.each do |category|
category.posts.limit(10)
end
But I also want to only get 10 posts per category, so the standard eager loading, which gets all the posts, does not suffice:
Category.includes(:posts).all
What is the best way to solve this problem? Is N+1 the only way to limit the amount of posts per category?
From the Rails docs
If you eager load an association with a specified :limit option, it will be ignored, returning all the associated objects
So given the following model definition
class Category < ActiveRecord::Base
has_many :posts
has_many :included_posts, -> { limit 10 }, class_name: "Post"
end
Calling Category.find(1).included_posts would work as expected and apply the limit of 10 in the query. However, if you try to do Category.includes(:included_posts).all the limit option will be ignored. You can see why this is the case if you look at the SQL generated by an eager load
Category.includes(:posts).all
Category Load (0.2ms) SELECT "categories".* FROM "categories"
Post Load (0.4ms) SELECT "posts".* FROM "posts" WHERE "posts"."category_id" IN (1, 2, 3)
If you added the LIMIT clause to the posts query, it would return a total of 10 posts and not 10 posts per category as you might expect.
Getting back to your problem, I would eager load all posts and then limit the loaded collection using first(10)
categories = Category.includes(:posts).all
categories.first.posts.first(10)
Although you're loading more models into memory, this is bound to be more performant since you're only making 2 calls against the database vs. n+1. Cheers.

eager loading the first record of an association

In a very simple forum made from Rails app, I get 30 topics from the database in the index action like this
def index
#topics = Topic.all.page(params[:page]).per_page(30)
end
However, when I list them in the views/topics/index.html.erb, I also want to have access to the first post in each topic to display in a tooltip, so that when users scroll over, they can read the first post without having to click on the link. Therefore, in the link to each post in the index, I add the following to a data attribute
topic.posts.first.body
each of the links looks like this
<%= link_to simple_format(topic.name), posts_path(
:topic_id => topic), :data => { :toggle => 'tooltip', :placement => 'top', :'original-title' => "#{ topic.posts.first.body }"}, :class => 'tool' %>
While this works fine, I'm worried that it's an n+1 query, namely that if there's 30 topics, it's doing this 30 times
User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 ORDER BY "users"."id" ASC LIMIT 1
Post Load (0.4ms) SELECT "posts".* FROM "posts" WHERE "posts"."topic_id" = $1 ORDER BY "posts"."id" ASC LIMIT 1 [["topic_id", 7]]
I've noticed that Rails does automatic caching on some of these, but I think there might be a way to write the index action differently to avoid some of this n+1 problem but I can figure out how. I found out that I can
include(:posts)
to eager load the posts, like this
#topics = Topic.all.page(params[:page]).per_page(30).includes(:posts)
However, if I know that I only want the first post for each topic, is there a way to specify that? if a topic had 30 posts, I don't want to eager load all of them.
I tried to do
.includes(:posts).first
but it broke the code
This appears to work for me, so give this a shot and see if it works for you:
Topic.includes(:posts).where("posts.id = (select id from posts where posts.topic_id = topics.id limit 1)").references(:posts)
This will create a dependent subquery in which the posts topic_id in the subquery is matched up with the topics id in the parent query. With the limit 1 clause in the subquery, the result is that each Topic row will contain only 1 matching Post row, eager loaded thanks to the includes(:post).
Note that when passing an SQL string to .where, that references an eager loaded relation, the references method should be appended to inform ActiveRecord that we're referencing an association, so that it knows to perform appropriate joins in the subsequent query. Apparently it technically works without that method, but you get a deprecation warning, so you might as well throw it in lest you encounter problems in future Rails updates.
To my knowledge you can't. Custom association is often used to allow conditions on includes except limit.
If you eager load an association with a specified :limit option, it will be ignored, returning all the associated objects. http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
class Picture < ActiveRecord::Base
has_many :most_recent_comments, -> { order('id DESC').limit(10) },
class_name: 'Comment'
end
Picture.includes(:most_recent_comments).first.most_recent_comments
# => returns all associated comments.
There're a few issues when trying to solve this "natively" via Rails which are detailed in this question.
We solved it with an SQL scope, for your case something like:
class Topic < ApplicationRecord
has_one :first_post, class_name: "Post", primary_key: :first_post_id, foreign_key: :id
scope :with_first_post, lambda {
select(
"topics.*,
(
SELECT id as first_post_id
FROM posts
WHERE topic_id = topics.id
ORDER BY id asc
LIMIT 1
)"
)
}
end
Topic.with_first_post.includes(:first_post)

Get first association from a has many through association

I'm trying to join the first song of each playlist to an array of playlists and am having a pretty tough time finding an efficient solution.
I have the following models:
class Playlist < ActiveRecord::Base
belongs_to :user
has_many :playlist_songs
has_many :songs, :through => :playlist_songs
end
class PlaylistSong < ActiveRecord::Base
belongs_to :playlist
belongs_to :song
end
class Song < ActiveRecord::Base
has_many :playlist_songs
has_many :playlists, :through => :playlist_songs
end
I would like to get this:
playlist_name | song_name
----------------------------
chill | baby
fun | bffs
I'm having a pretty tough time finding an efficient way to do this through a join.
UPDATE ****
Shane Andrade has lead me in the right direction, but I still can't get exactly what I want.
This is as far as I've been able to get:
playlists = Playlist.where('id in (1,2,3)')
playlists.joins(:playlist_songs)
.group('playlists.id')
.select('MIN(songs.id) as song_id, playlists.name as playlist_name')
This gives me:
playlist_name | song_id
---------------------------
chill | 1
This is close, but I need the first song(according to id)'s name.
Assuming you are on Postgresql
Playlist.
select("DISTINCT ON(playlists.id) playlists.id,
songs.id song_id,
playlists.name,
songs.name first_song_name").
joins(:songs).
order("id, song_id").
map do |pl|
[pl.id, pl.name, pl.first_song_name]
end
I think this problem would be improved by having a a stricter definition of "first". I'd suggest adding a position field on the PlaylistSong model. At which point you can then simply do:
Playlist.joins(:playlist_song).joins(:song).where(:position => 1)
What you are doing above with joins is what you would do if you wanted to find every playlist with a given name and a given song. In order to collect the playlist_name and first song_name from each playlist you can do this:
Playlist.includes(:songs).all.collect{|play_list| [playlist.name, playlist.songs.first.name]}
This will return an array in this form [[playlist_name, first song_name],[another_playlist_name, first_song_name]]
I think the best way to do this is to use an inner query to get the first item and then join on it.
Untested but this is the basic idea:
# gnerate the sql query that selects the first item per playlist
inner_query = Song.group('playlist_id').select('MIN(id) as id, playlist_id').to_sql
#playlists = Playlist
.joins("INNER JOIN (#{inner_query}) as first_songs ON first_songs.playlist_id = playlist.id")
.joins("INNER JOIN songs on songs.id = first_songs.id")
Then rejoin back to the songs table since we need the song name. I'm not sure if rails is smart enough to select the song fields on the last join. If not you might need to include a select at the end that selects playlists.*, songs.* or something.
Try:
PlaylistSong.includes(:song, :playlist).
find(PlaylistSong.group("playlist_id").pluck(:id)).each do |ps|
puts "Playlist: #{ps.playlist.name}, Song: #{ps.song.name}"
end
(0.3ms) SELECT id FROM `playlist_songs` GROUP BY playlist_id
PlaylistSong Load (0.2ms) SELECT `playlist_songs`.* FROM `playlist_songs` WHERE `playlist_songs`.`id` IN (1, 4, 7)
Song Load (0.2ms) SELECT `songs`.* FROM `songs` WHERE `songs`.`id` IN (1, 4, 7)
Playlist Load (0.2ms) SELECT `playlists`.* FROM `playlists` WHERE `playlists`.`id` IN (1, 2, 3)
Playlist: Dubstep, Song: Dubstep song 1
Playlist: Top Rated, Song: Top Rated song 1
Playlist: Last Played, Song: Last Played song 1
This solution has some benefits:
Limited to 4 select statements
Does not load all playlist_songs - aggregating on db side
Does not load all songs - filtering by id's on db side
Tested with MySQL.
This will not show empty playlists.
And there could be problems with some DBs when playlists count > 1000
just fetch the song from the other side :
Song
.joins( :playlist )
.where( playlists: {id: [1,2,3]} )
.first
however, as #Dave S. suggested, "first" song in a playlist is random unless you explicitly specify an order (positioncolumn, or anything else) because SQL does not warrant the order in which the records are returned, unless you explicitly ask it.
EDIT
Sorry, I misread your question. I think that indeed a position column is necessary.
Song
.joins( :playlist )
.where( playlists: {id: [1,2,3]}, songs: {position: 1} )
If you do not want any position column at all, you can always try to group the songs by playlist id, but you'll have to select("songs.*, playlist_songs.*"), and the "first" song is still random. Another option is to use the RANK window function, but it is not supported by all RDBMS (for all i know, postgres and sql server do).
you can create a has_one association which, in effect, will call the first song that is associated to the playlist
class PlayList < ActiveRecord::Base
has_one :playlist_cover, class_name: 'Song', foreign_key: :playlist_id
end
Then just use this association.
Playlist.joins(:playlist_cover)
UPDATE: didn't see the join table.
you can use a :through option for has_one if you have a join table
class PlayList < ActiveRecord::Base
has_one :playlist_song_cover
has_one :playlist_cover, through: :playlist_song_cover, source: :song
end
Playlyst.joins(:playlist_songs).group('playlists.name').minimum('songs.name').to_a
hope it works :)
got this :
Product.includes(:vendors).group('products.id').collect{|product| [product.title, product.vendors.first.name]}
Product Load (0.5ms) SELECT "products".* FROM "products" GROUP BY products.id
Brand Load (0.5ms) SELECT "brands".* FROM "brands" WHERE "brands"."product_id" IN (1, 2, 3)
Vendor Load (0.4ms) SELECT "vendors".* FROM "vendors" WHERE "vendors"."id" IN (2, 3, 1, 4)
=> [["Computer", "Dell"], ["Smartphone", "Apple"], ["Screen", "Apple"]]
2.0.0p0 :120 > Product.joins(:vendors).group('products.title').minimum('vendors.name').to_a
(0.6ms) SELECT MIN(vendors.name) AS minimum_vendors_name, products.title AS products_title FROM "products" INNER JOIN "brands" ON "brands"."product_id" = "products"."id" INNER JOIN "vendors" ON "vendors"."id" = "brands"."vendor_id" GROUP BY products.title
=> [["Computer", "Dell"], ["Screen", "Apple"], ["Smartphone", "Apple"]]
You could add activerecord scope to your models to optimize how the sql queries work for you in the context of the app. Also, scopes are composable, thus make it easier to obtain what you're looking for.
For example, in your Song model, you may want a first_song scope
class Song < ActiveRecord::Base
scope :first_song, order("id asc").limit(1)
end
And then you can do something like this
playlists.songs.first_song
Note, you may also need to add some scopes to your PlaylistSongs association model, or to your Playlist model.
You didn't say if you had timestamps in your database. If you do though, and your records on the join table PlaylistSongs are created when you add a song to a playlist, I think this may work:
first_song_ids = Playlist.joins(:playlist_songs).order('playlist_songs.created_at ASC').pluck(:song_id).uniq
playlist_ids = Playlist.joins(:playlist_songs).order('playlist_songs.created_at ASC').pluck(:playlist_id).uniq
playlist_names = Playlist.where(id: playlist_ids).pluck(:playlist_name)
song_names = Song.where(id: first_song_ids).pluck(:song_name)
I believe playlist_names and song_names are now mapped by their index in this way. As in: playlist_names[0] first song name is song_names[0], and playlist_names[1] first song name is song_names[1] and so on. I'm sure you could combine them in a hash or an array very easily with built in ruby methods.
I realize you were looking for an efficient way to do this, and you said in the comments you didn't want to use a block, and I am unsure if by efficient you meant an all-in-one query. I am just getting used to combining all these rails query methods and perhaps looking at what I have here, you can modify things to your needs and make them more efficient or condensed.
Hope this helps.

Rails 3 Limiting Included Objects

For example I have a blog object, and that blog has many posts. I want to do eager loading of say the first blog object and include say the first 10 posts of it. Currently I would do #blogs = Blog.limit(4) and then in the view use #blogs.posts.limit(10). I am pretty sure there is a better way to do this via an include such as Blog.include(:posts).limit(:posts=>10). Is it just not possible to limit the number of included objects, or am I missing something basic here?
Looks like you can't apply a limit to :has_many when eager loading associations for multiple records.
Example:
class Blog < ActiveRecord::Base
has_many :posts, :limit => 5
end
class Post < ActiveRecord::Base
belongs_to :blog
end
This works fine for limiting the number of posts for a single blog:
ruby-1.9.2-p290 :010 > Blog.first.posts
Blog Load (0.5ms) SELECT `blogs`.* FROM `blogs` LIMIT 1
Post Load (0.6ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`blog_id` = 1 LIMIT 5
However, if you try to load all blogs and eager load the posts with them:
ruby-1.9.2-p290 :011 > Blog.includes(:posts)
Blog Load (0.5ms) SELECT `blogs`.* FROM `blogs`
Post Load (1.1ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`blog_id` IN (1, 2)
Note that there's no limit on the second query, and there couldn't be - it would limit the number of posts returned to 5 across all blogs, which is not at all what you want.
EDIT:
A look at the Rails docs confirms this. You always find these things the minute you've figured them out :)
If you eager load an association with a specified :limit option, it
will be ignored, returning all the associated objects
You need to limit the number of posts in your blog model like this:
class Blog < ActiveRecord::Base
has_many :included_posts, :class_name => 'Post', :limit => 10
has_many :posts
end
So then you can do:
$ Blog.first.included_posts.count
=> 10
$ Blog.first.posts.count
=> 999

Resources