Rails 4 conditional render cause N+1 query - ruby-on-rails

I have a grid of posts where a user can like or unlike causing N+1 queries.
I'm rendering a collections of post which partial's has:
<%= post.title %>
<% if current_user.liking?(post) %>
<%= render partial: 'posts/unlike' %>
<% else %>
<%= render partial: 'posts/like' %>
<% end %>
...
User Controller:
def show
#user = User.find(params[:id])
#feed = #user.posts.paginate(page: params[:page])
end
This is causing one query for each post. I though using .include(:likes) but I don't know how to do it for 'current_user'
Edit:
I ended up creating a second query for likes thats belong to current_user
#liking_items = #feed.includes(:likes).where(likes: { user_id: current_user })

Since you have the current_user, you can just add a where:
#user.post.includes(:likes).where(likes: { user_id: current_user })
(I'm assuming that the "like owner" is user_id).

Related

Merging three Active Record arrays

I'm trying to merge three Active Record arrays in a Rails 5 app so that I have a nice collection of jobs, forum threads and blogs on my home page.
I have the following code:
application_controller.rb
def home
#blogs = Blog.limit(6)
#jobs = Job.where(approved: true).limit(6)
#forum_threads = ForumThread.includes(:forum_posts).limit(6)
#everything = #blogs + #jobs + #forum_threads
end
home.html.erb
<% #everything.sort_by(&:created_at).reverse.each do |item| %>
<% if item.is_a?(Job) %>
<%= render partial: "application/partials/home_job", locals: {item: item} %>
<% elsif item.is_a?(ForumThread) %>
<%= render partial: "application/partials/home_forum", locals: {item: item} %>
<% elsif item.is_a?(Blog) %>
<%= render partial: "application/partials/home_blog", locals: {item: item} %>
<% end %>
<% end %>
The problem I'm having is that this code doesn't display the records in date order by created_by, instead I have a rather random collection of jobs, forum threads and blogs starting at a seemingly random date.
If I add, say, a new job, it doesn't appear in the collection displayed on /home page. However, if I delete all records from the db and start adding new records then the code works fine and displays the posts in the correct order with the behaviour I expect.
I can't push this code live to Heroku because I can't delete all the records that already exist in production. It's almost like there's some kind of cache that needs clearing out. Does anyone know what's going on?
#blogs = Blog.order(created_at: :desc).limit(6)
etc.
Problem 1: Getting the right records from the database
Option A: If you will always be sorting each model by the created_at value (a common desire), add a default_scope to each model (Rails 4+ version below). Your limit calls in the controller will automatically take advantage of the default scope.
app/models/blog.rb
class Blog < ActiveRecord::Base
default_scope { order created_at: :desc }
...
end
Option B: If you only do this in certain circumstances, but you do it for several models, I like to extract that into a Timestamped module (below). You will need to use the most_recent method in your controller when extracting records from the database to ensure you're getting the most recent ones.
app/models/concerns/timestamped.rb
module Timestamped
extend ActiveSupport::Concern
included do
scope :most_recent, -> { order created_at: :desc }
scope :least_recent, -> { order created_at: :asc }
scope :most_fresh, -> { order updated_at: :desc }
scope :least_fresh, -> { order updated_at: :asc }
end
end
class Blog < ActiveRecord::Base
include Timestamped
...
end
Problem 2: Sorting the array
Even with a simple case like this, I'd recommend adding an array extension that matches the most_recent method that timestamped.rb defines for ActiveRecord::Relations.
lib/array_extensions.rb
class Array
def most_recent
sort { |a, b| b.created_at <=> a.created_at }
end
end
and then require the extension with an initializer:
config/initializers/extensions.rb
require 'array_extensions'
Problem 3: Keeping the controller clean.
Generally each controller action should only set up one instance variable, and in this case it looks like you are not even using the #blogs, #jobs, and #forum_threads variables in the views. Vivek's answer solves this, although I'd do the flattening and sorting logic in the controller:
def home
#posts = Blog.most_recent.limit(6) + Job.approved.most_recent.limit(6) + ForumThread.most_recent.includes(:forum_posts).limit(6)
#posts = #posts.most_recent
end
Problem 4: Minimize if/then logic in your view
Instead of this:
<% #everything.sort_by(&:created_at).reverse.each do |item| %>
<% if item.is_a?(Job) %>
<%= render partial: "application/partials/home_job", locals: {item: item} %>
<% elsif item.is_a?(ForumThread) %>
<%= render partial: "application/partials/home_forum", locals: {item: item} %>
<% elsif item.is_a?(Blog) %>
<%= render partial: "application/partials/home_blog", locals: {item: item} %>
<% end %>
<% end %>
Do this:
<% #everything.sort_by(&:created_at).reverse.each do |item| %>
<%= render "application/partials/home_#{item.class.name.underscore}", item: item %>
<% end %>
And make sure your partials are named appropriately
You can do like this:
def home
#collections=[]
#collections << Blog.limit(6)
#collections << Job.where(approved: true).limit(6)
#collections << ForumThread.includes(:forum_posts).limit(6)
end
<% #collections.flatten.sort_by(&:created_at).reverse.each do |item| %>
....iteration here ....
<% end %>
if i understood your question correctly, you want to sort the array after you merged it by date. I would do it like that:
#everything = #everything.sort {|x| x.created_at }
Hope that helps.

Combine the current user posts & followers posts w/Acts_as_Follower Rails 4

I'm trying to show the current_user posts and the users that he or she is following posts in a single view. For whatever reason, the posts that I've already created are not loading into the view. There are currently 5 in the database.
feed controller
def feed
following_ids = current_user.following_users.map(&:id)
#following_activities = Post.where('user_id in (?)', following_ids.push).order('created_at desc').paginate(page: params[:page])
end
feed.html.erb
<% if #following_activities.any? %>
<% #following_activities.each do |user| %>
<%= link_to(image_tag(user.post.avatar.url(:thumb), style: 'text-decoration: none;', class: 'round-image'), user_path(user)) %>
<%= user.post.username %>
<%= user.post.body %>
<%= image_tag(user.post.photo.url(:medium), style: '') %>
<% end %>
<% else %>
<h1>No new posts</h1>
<% end %>
It seems you are pushing nothing to the array of ids, you shall push current_user.id. You can make your code slightly cleaner like:
user_ids = current_user.following_users.pluck(:id).push(current_user.id)
#following_activities = Post.where(user_id: user_ids).order('created_at desc').paginate(page: params[:page])
In three steps, I was able to combine both, and now it's working properly.
def feed
following_ids = current_user.following_users.map(&:id)
following_ids << current_user.id
#following_activities = Post.where(user_id: following_ids).order("created_at desc").paginate(page: params[:page])
end

.each loop returns []. Form / controller issue

I have RoR 4.2.0beta. (Although it s irrelevant as this is a beginer problem).
My form does not insert in the database the "propuneres" that I am creating trough it. And as a result they do not show in the index page when I get redirected to it. They show up when I create them through the console.
class PropuneresController < ApplicationController
before_action :prop_params
def new
#user = User.find(params[:user_id])
#propunere = #user.propuneres.build
end
def create
#user= User.find(params[:user_id])
#propunere = #user.propuneres.new(params[:prop_params])
#propunere.save
if #propunere.empty?
render 'new'
else
redirect_to user_propuneres_path
end
end
def index
#user = User.find(params[:user_id])
#propunere = #user.propuneres(params[:prop_params])
end
private
def prop_params
params.require(:propunere).permit(:titlu, :body)
end
end
new.html.erb
<h2> Propunere Nouă </h2>
<%= form_for #propunere do |f| %>
<ul>
<% #propunere.errors.full_messages.each do |error| %>
<li><%= error %></li>
<% end %>
</ul>
<p>
<%= f.label :titlu %><br />
<%= f.text_field :titlu %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
index.html.erb
<h2> Propuneri: </h2>
<% #propunere.each do |p| %>
<%= p.titlu %>
<%= p.body %>
<% end %>
Not sure if its relevant, but you have code
#propunere.save
if #propunere.empty?
render 'new'
else
redirect_to user_propuneres_path
end
Object #prorunere will never be empty, since you have
#propunere = #user.propuneres.new, which assigneds user_id to your #propunere object and
render 'new' will never be rendered, therefore you wont see any validation errors and never find out why your record wasnt created
Also since you have that piece of code, and dont see errors, this is what most like broke your code
#user.propuneres.new(params[:prop_params]) - you should use your permitted params, so it'd look like
#propunere = #user.propuneres.new(prop_params)
I've cloned your repo, here's the problem: in new.html.erb you had
&lt%= form_for #propunere, url: new_user_propunere_path(#user, #propunere), html: { method: :get } do |f| %&gt
Both the url and the method are wrong. user_propuneres_path will give you the correct url for the create action and the correct method is :post, not :get. This is why you never reached the create action.
You also need to change from #propunere = #user.propuneres.new(params[:propunere]) to #propunere = #user.propuneres.new(prop_params), otherwise you'll get a ActiveModel::ForbiddenAttributesError exception.
You can see all the routes in the app by running rake routes in the terminal.
I don't think you need the params in your index action:
#propunere = #user.propuneres
and it would be more logical to write it in plural since you have many of them.
Edit:
As Avdept suggested your create action should look like this:
def create
#user = User.find(params[:user_id])
#propunere = #user.propuneres.new(prop_params)
respond_to do |format|
if #propunere.save
format.html { redirect_to user_propuneres_path(#user), notice: 'Your propunere has been saved' }
else
format.html { render :new }
end
end
end
Do you have any validations for the Propunere model? Maybe the model is invalid. You can use
the create! method instead of create for testing, because it will throw an exception if the object cannot be saved. Also try puts #propunere.inspect before persisting it and check that the contents of the object are ok, the output will be shown in the development log.

List of users showing the same information

I am showing the subscription information for all users. The issue I have is that it is not displaying the correct information. It is showing the current_user subscription information for all the Users. When it should be displaying each users unique subscription information.
Admin views:
<% #users.each do |user| %>
<li>
<%= link_to user.username, user %>
|<%= #user.subscription.id %>|<%= #user.subscription.plan.name %>| <%= #user.subscription.created_at.strftime("%B %e, %Y") %>
</li>
<% end %>
Admin controller:
def index
#user = current_user
#users = #user.present? ? User.where('id != ?',#user.id) : User.all
render layout: 'new_application'
end
def show
#user = User.find_by(username: params[:id])
end
Error: NoMethodError: undefined method `id' for nil:NilClass
It seems like the template should refer to user, not #user.
Update:
As #rlecaro2 mentioned, it's possible you've got some users with any subscriptions. In the case, calling .subscription on the user variables returns nil and you crash while asking for its id.
<% #users.each do |user| %>
<li>
<%= link_to user.username, user %>
<% if user.subscription %>
|<%= user.subscription.id %>|<%= user.subscription.plan.name %>| <%= user.subscription.created_at.strftime("%B %e, %Y") %>
<% end %>
</li>
<% end %>
Also since you're asking every user for its subscription and later on for the subscription's plan, you should explicitly load these to avoid unnecessary queries. For Rails 4, the Admin controller should look like this:
def index
#users = User.includes(subscription: :plan)
#users = #users.where.not(id: current_user.id) if current_user
render layout: 'new_application'
end
Note that it's not necessary to carry the current_user via #user. You can refer to it in the template directly.

nested attribute pagination error (kaminari)

I'm pretty sure that this is a simple fix but I'm not seeing it. I have an app where I'd like to show a music_videos comments. Below are my controllers:
def show
#music_video = MusicVideo.find(params[:id])
#comment = Comment.new
#comments = #music_video.comments.page(params[:page]).per(3)
end
The above is my music video controller.
def create
#music_video = MusicVideo.find(params[:music_video_id])
#comment = #music_video.comments.build(comment_params)
if #comment.save
flash[:notice] = "Comment Submitted"
redirect_to music_video_path(#music_video)
else
render 'music_videos/show'
end
end
def destroy
#comment = Comment.find(params[:id])
#comment.destroy
redirect_to root_path, notice: "Comment Deleted"
end
private
def comment_params
params.require(:comment).permit(:body)
end
Above is my comments controller
Finally my show page:
<div class="comments_row">
<% #music_video.comments.each do |comment| %>
<% if user_signed_in? && current_user.admin? %>
<p class="comment"><%= comment.body %></p>
<%= link_to 'Delete Comment', music_video_comment_path(#music_video,comment),
method: :delete %>
<% else %>
<p class="comment"><%= comment.body %></p>
<% end %>
<%end%>
</div>
<%= paginate #comments %>
I'm pretty sure something is wrong with my controllers, but I'm not sure exactly what it is. #comments is in the correct controller (MusicVideo) within the correct CRUD operation (show). Currently I have six comments in a particular show page and the pagination shows up just fine but the six comments are not paginated. Any thoughts?
EDIT-------------
I figured out one the problem but stumbled on a new one. I figured out that in my controller I am declaring #comments = pagination etc. etc. when in my views there is no #comments to paginate. The problem is now that when I use
<%= paginate #comment %>
the code will break.the problem now that I'm having is what variable to paginate. Trying this code will also break
<%= paginate #music_video.comments %>
Any recommendations?
I set up a test application using the kaminari gem for pagination. This is what my my music video controller's show action looks like:
def show
#music_video = MusicVideo.find(params[:id])
#comments = #music_video.comments.page(params[:page]).per(3)
end
And here is what my show view looks like:
<p id="notice"><%= notice %></p>
<p>
<strong>Name:</strong> <%= #music_video.name %>
</p>
<% #comments.each do |comment| %>
<p>
Comment: <%= comment.text %>
</p>
<% end %>
<%= paginate #comments %>
<%= link_to 'Edit', edit_music_video_path(#music_video) %> |
<%= link_to 'Back', music_videos_path %>
It is working and the pagination is showing up for me.
I think one thing i see directly is that you should use <% #comments.each do |comment| %> instead of <% #music_video.comments.each do |comment| %> because the way you have it now it will display all comments for the video regardless of what page you are on. If you had 6 comments and wanted 3 per page you would see the pagination with the two pages because you're running your pagination based off of #comments and you would end up seeing all 6 comments on both pages because you're doing your .each with #music_videos.comments.each.
So, at least using #comments in both places would be a start. And make sure you're using <%= paginate #comments %> for the pagination. If you use this in your controller and view what do you get? Do you see any comments?
Also, Ryan Bates has a great screencast on Kaminari as well: http://railscasts.com/episodes/254-pagination-with-kaminari (that site is a great resource for rails questions)

Resources