How to avoid database calls from the View in Rails? - ruby-on-rails

Let's assume this example:
Model:
class User < ActiveRecord::Base
has_many :posts
end
Controller:
def index
#users = User.all
end
View:
<ul>
<% #users.each do |u| %>
<li>
Username: <%= u.username %><br />
<%= pluralize(u.posts.count, "post") %>
</li>
<% end %>
</ul>
From what I understand (by looking at WEBrick in the command line), it does not do a database call for u.username, but it does for u.posts.count for each cycle in the loop. I want to avoid that, so I store the "posts" in an instance variable in the controller (#posts = Post.all, for example) and replace the u.posts.count with #posts.where(:user_id => u.id).count, but it still does a database call for each cycle. Doesn't the application have all of the information stored in the #posts array in the controller?
Note: None of this is specific to my case (I am not displaying a list of users); I'm simply using this to serve as an example.

One way to deal with this is a counter_cache. You effectively add a new posts_count property to your User model, and update it every time you modify the user's posts. Rails makes this easy to do with the counter_cache association option:
class User < ActiveRecord::Base
has_many :posts, counter_cache: true
end
With that setup, calling u.posts.size will instead return the count stored in the user model instead of hitting the database (make sure you use size, not count).
For more info on the :counter_cache option, check the Rails Association Basics guide (section 4.1.2.3). This blog post covers how to actually go about adding one in, including the migration and initialising the values.
The second way you could do this, would be to load all the posts in your controller method, as you attempted, but it could be a bit neater to simple eager load them instead (#users = User.includes(:posts).all). The reason it didn't work for you was because you were using count not size — count always hits the database with a SELECT COUNT(*) statement, whereas size is more intelligent and will avoid hitting the database if possible. (This is why you should make sure you use size when using a counter_cache too.)
This is valid too, but if you're not actually going to use the posts in any way though, it may be preferable to avoid pulling them all from the database, hence the appeal of the counter_cache approach.

Another option is to do a SQL query with a subselect for your count. The following query assumes User.id = Post.user_id
#users = User.select("u.username").
select("count(p.id) AS post_count").
from("users u").
joins("LEFT JOIN posts p ON u.id = p.user_id").
group("u.id").
all
Then in your view:
<% #users.each do |u| %>
<li>
Username: <%= u.username %><br />
<%= pluralize(u[:post_count], "post") %>
</li>
This should be your output to the console:
Started GET "/reports" for 127.0.0.1 at 2013-11-28 16:33:31 -0700
Processing by ReportsController#index as HTML
User Load (0.8ms) SELECT u.username, count(p.id) AS post_total FROM users u LEFT JOIN posts p ON u.id = p.user_id GROUP BY u.id
Rendered users/index.html.erb within layouts/application (0.5ms)
Completed 200 OK in 10ms (Views: 7.3ms | ActiveRecord: 1.0ms)

Related

Insert multirows from checkboxes [duplicate]

I am stuck in that although my array parameter is being captured, it fails to insert it into the database. I do not get an unpermitted parameters error or anything. It just fails to recognize the array when inserting to the DB.
What I would like to do: Capture any box that is checked off, and insert the data as separate rows into the database.
Here is what I have:
/subscribe/categories/2
<div>
<%= simple_form_for #subscription do |f| %>
<div class="form-inputs">
<%= f.hidden_field :dashboard_id, value: 1 %>
<%= f.hidden_field :category_id, value: #category.id %>
<%= f.collection_check_boxes :feed_id, Feed.where("category_id = ?", #category), :id, :name %>
</div>
<div class="form-actions">
<%= f.button :submit %>
</div>
<% end %>
</div>
CategoriesController
def show
#subscription = Subscription.new
end
SubscriptionsController
def subscription_params
params.require(:subscription).permit(:dashboard_id, :category_id, :feed_id => [])
end
When submitted, here is the console output:
Processing by SubscriptionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"Zw2VkwujDLQjV4krjPF8N1EiYo5L/XOrUwedlHCvwB0=", "subscription"=>{"dashboard_id"=>"1", "category_id"=>"2", "feed_id"=>["3", "4", ""]}, "commit"=>"Create Subscription"}
(0.2ms) BEGIN
SQL (1.6ms) INSERT INTO `subscriptions` (`category_id`, `created_at`, `dashboard_id`, `updated_at`) VALUES (2, '2014-01-06 02:17:41', 1, '2014-01-06 02:17:41')
(116.6ms) COMMIT
Redirected to http://localhost:3000/subscriptions/3
Completed 302 Found in 173ms (ActiveRecord: 119.3ms)
Two questions:
Why is there an extra "" for my feed_id array? (Only 2 possible checkboxes)
Why am I not capturing the array to insert it into the database?
Thanks!
The reason your array is not being inserted into the database is that Active Record currently does not support the Postgresql array type. In order to insert these as separate rows the check-boxes need to be represented as individual instances of a model.
Possibly something like...
Category < ActiveRecord::Base
has_many: feeds
...
end
Feed < ActiveRecord::Base
belongs_to: category
...
end
Now this would also mean that you would need to use the form_tag helper instead of the form_for. This would allow you to create a composite form consisting of multiple individual objects. Inserting this would just mean iterating and inserting over each object; giving you separate rows. Hope this helps.
For anyone that wants to know how to do this, here is one solution I've come up with. Everything in my first post remains the same. In my SubscriptionsController (from which the form is created), here is my create action:
def create
dashboard = params[:subscription][:dashboard_id]
category = params[:subscription][:category_id]
feed = params[:subscription][:feed_id]
#subscription = feed.map { |subscribe| Subscription.create(dashboard_id: dashboard, category_id: category, feed_id: subscribe) }
end
Works as advertised. If anyone thinks for some reason that I am overlooking this is a terrible idea, please comment.

Rails activerecord - how to inherit all child associations?

So in my application I have the models People and Outfits. In my show controller for people, I get the list like this:
#people = Person.where("description LIKE ?", "%#{params[:description]}%")
And in my view I show the outfits of each person like this:
<% #people.each do |person| %>
<p> Name: <%= person.name %> </p>
<% #outfits = person.outfits %>
<% #outfits.each do |outfit|
<p> Name: <%= outfit.name %> </p>
<p> Description: <%= outfit.description %> </p>
<% end %>
<% end %>
But loading the outfits for each person, as I load many people on the page, takes too long. Is there some way I can inherit the outfits of each person so I don't have to wait so long for the page to load? Or is the only way to speed this up to make an index between outfits and people? Thanks for any help
Use a join to load the associated records:
#people = Person.eager_load(:outfits)
.where("description LIKE ?", "%#{params[:description]}%")
.limit(20) # optional
Otherwise you have what is called a N+1 query issue where each iteration through #people will cause a separate database query to fetch outfits.
And yes the outfits.person_id or whatever column that creates the association should have a foreign key index. Using the belongs_to or references macro in the migration will do this by default:
create_table :outfits do |t|
t.belongs_to :person, foreign_key: true
end
Active Record Query Interface - Eager Loading Associations
Making sense of ActiveRecord joins, includes, preload, and eager_load
you should set a limit like this:
#people = Person.where("description LIKE ?", "%#{params[:description]}%").limit(20)
change the number according to your preference.
You can use .joins or .includes
If you have a table full of Person and you use a :joins => outfits to pull in all the outfit information for sorting purposes, etc it will work fine and take less time than :include, but say you want to display the Person along with the outfit name, description, etc. To get the information using :joins, it will have to make separate SQL queries for each user it fetches, whereas if you used :include this information is ready for use.
Solution
Person.includes(:outfits).where("description LIKE ?", "%#{params[:description]}%")

Select multiple values from associated model rails

Assuming I have this association
User have_many posts
Post belongs_to user
User Post
----------------
id id
name title
user_id
How to list only post title and username with includes/joins ?
(list of posts [title - username])
#posts = Post.includes(:user).select('........')
don't offer this
#posts = Post.all.each {|p| p.user.username}
__________________UP_____________________
It worked for joining 2 tables.
What if I want to use it for more complex example?
check out my prev question optimize sql query rails
#Humza's answer partly worked.
it might be something like this
#posts = Post.joins(:user, :category).paginate(:page => params[:page]).order("created_at DESC")
but It doesn't display posts that don't have category
I also need to display gravatar but I think I can just use user.email as usr_email and use gravatar_for (post.usr_email) but I'll have to customize gravatar helper for this.
posts_controller.rb
def index
#posts = Post.includes(:user).includes(:comments).paginate(:page => params[:page]).order("created_at DESC")
end
index.html.erb
<%= render #posts %>
_post.html.erb
<%= gravatar_for post.user, size:20 %>
<%= link_to "#{post.title}", post_path(post) %>
<%= time_ago_in_words(post.created_at) %>
<%= post.comments.count %>
<%= post.category.name if post.category %>
Take a look at pluck.
Post.joins(:user).pluck(:title, :name)
Note that it works in this case because there's no ambiguity regarding the name column, you might want to specify the table explicitly (pluck(:title, "users.name")).
includes is used in case of eager-loading. You need joins in this case.
posts = Post.joins(:user).select("posts.title AS title, users.name AS username")
You can access the values then in the following way:
post = posts.first
post.title # will give the title of the post
post.username # will give the name of the user that this post belongs to
If you can pluck multiple columns, then the following might be more useful to you:
posts = Post.joins(:user).pluck("posts.title", "users.name")
The result will be a 2D array, with each element being an array of the form [post_title, post_username]
Post.joins(:user, :category)
but It doesn't display posts that don't have category
That's because joins uses INNER JOIN to join the tables together. If you want to everything from Post even though the particular record doesn't have its counterpart in the other table, you need to use LEFT JOIN. Unfortunately ActiveRecord doesn't have a nice way of generating it and you will need to do that manually:
Post.joins("LEFT OUTER JOIN categories ON categories.post_id = posts.id")...
See A Visual Explanation of SQL Joins for more information.
You can call array methods on a scope so:
Post.includes(:user).map { |p| [p.title, p.user.name] }
will get the posts with included user and map each post to a tuple of the post title and the user name.
That may not entirely answer your question as I think you might want to restrict the results of the query to just the required fields in which case, I think you can add a .select('title', 'users.name') to the query. (Not in a position to test at the moment)

Rails, is it ok to look up objects in views?

A controller points to a view. In that view is it acceptable to find objects <% #XXX = XXXX.where(..... %> or is that bad?
Trying to work through performance issues which is why I ask. Thanks
Putting query logic in the model has more to do with maintainability then it does with performance. Since most of the ActiveRecord/ARel logic deals with lightweight relation objects that only trigger the actual query based on certain methods, generally those provided via Enumerable (each/map/inject/all/first), which are usually called from the view anyway, the actual query gets triggered in the view, and not anywhere else.
Here's an example of the difference between limit(3) and first(3) from an app I'm working on atm.
ruby-1.9.2-p180 :018 > PressRelease.limit(3).is_a? ActiveRecord::Relation
=> true
ruby-1.9.2-p180 :019 > PressRelease.first(3).is_a? ActiveRecord::Relation
PressRelease Load (2.8ms) SELECT "press_releases".* FROM "press_releases" ORDER BY published_at DESC
=> false
As you can see, limit does not actually trigger a query, where first does.
When it comes to performance you are usually trying to ensure that your queries are not executed in your controller/model so that you can wrap them in a cache block within your view, thus eliminating that query from most requests. In this case you really want to make sure your not executing the query in your controller by calling any of the Enumerable methods.
A quick example of a blog that lists the last 10 blog posts on the home page that is setup with caching might look like this.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
# Something like this would trigger the query at this point and should be
# avoided in the controller
# #posts = Post.first(10)
# So #posts here will be the Relation returned from the last_ten scope, not
# an array
#posts = Post.last_ten
end
...
end
# app/models/post.rb
class Post < ActiveRecord::Base
# Will return an ActiveRecord::Relation
scope :last_ten, order('created_at DESC').limit(10)
end
# app/views/posts/index.html.erb
<ul>
# The query will actually trigger within the cache block on the call to each,
# preventing the query from running each time and also reducing the template
# rendering within the cache block.
<%= cache(posts_cache_key) do %>
<% #posts.each do |post| %>
..
<% end %>
<% end %>
</ul>
For clarity, all this would be the exact same as doing
# app/views/posts/index.html.erb
<ul>
<%= cache(posts_cache_key) do %>
<% Post.order('created_at DESC').limit(10).each do |post| %>
...
<% end %>
<% end %>
</ul>
Except that if you now want to modify the logic for how it pulls the query, say you wanted to add something like where(:visible => true).where('published_at' <= Time.now) your jumping into your view instead of making changes in the model where the logic should be. Performance wise the difference is insignificant, maintenance-wise the latter turns into a nitemare rather quickly.

Ruby on Rails -- Saving and updating an attribute in a join table with has many => through

To simplify things, I have 3 tables :
Person
has_many :abilities, through => :stats
Ability
has_many :people, through => :stats
Stats
belongs_to :people
belongs_to :abilities
Stats has an extra attribute called 'rating'.
What I'd like to do is make an edit person form that always lists all the abilities currently in the database, and lets me assign each one a rating.
For the life of me, I can't figure out how to do this. I managed to get it to work when creating a new user with something like this:
(from the people controller)
def new
#character = Character.new
#abilities = Ability.all
#abilities.each do |ability|
#person.stats.build(:ability_id => ability.id )
end
end
From the people form:
<% for #ability in #abilities do %>
<%= fields_for "person[stats_attributes]" do |t| %>
<div class="field">
<%= t.label #ability.name %>
<%= t.hidden_field :ability_id, :value => #ability.id, :index => nil %>
<%= t.text_field :rating, :index => nil %>
</div>
<% end %>
<% end %>
This successfully gives me a list of abilities with ratings boxes next to them, and lets me save them if i'm making a new user.
The problem is that if I then load up the edit form (using the same form partial), it doesn't bring back the ratings, and if I save, even with the exact same ratings, it creates duplicate entries in the stats table, instead of updating it.
I realize I'm a terrible programmer and I'm probably doing this the wrong way, but how do I get the edit form to recall the current ratings assigned to each ability for that user, and secondly how do i get it to update the rating instead of duplicating it if the combination of person and ability already exists?
Shouldn't that be
Character
has_many :stats
has_many :abilities, through => :stats
Ability
has_many :stats
has_many :characters, through => :stats
Stat
belongs_to :character
belongs_to :ability
?
Also, is it Person or Character? You refer variously to both. (I'm going to go with Character in my answer)
I think you've fallen foul of the "I'll try to make a simplified version of my schema in order to attempt to illustrate a problem but instead make things more complex and muddle the issue by screwing it up so it doesn't make sense" syndrome. Anyway, there's a couple of issues i can see:
1) first thing is that you're adding all the possible abilities to a character as soon as they're created. This is silly - they should start out with no abilities by default and then you create join table records (stats) for the ones they do have (by ticking checkboxes in the form).
2) A simple way to manipulate join records like this is to leverage the "ability_ids=" method that the has_many :abilities macro gives you - referred to as "collection_ids=" in the api http://railsbrain.com/api/rails-2.3.2/doc/index.html?a=M001885&name=has_many
In other words, if you say
#character.ability_ids = [1,12,30]
then that will make joins between that character and abilities 1, 12 and 30 and delete any other joins between that character and abilities not in the above list. Combine this with the fact that form field names ending in [] put their values into an array, and you can do the following:
#controller
def new
#character = Character.new
#abilities = Ability.all
end
#form
<% #abilities.each do |ability| %>
<div class="field">
<%= t.label #ability.name %>
<%= check_box_tag "character[ability_ids][]" %>
</div>
<% end %>
#subsequent controller (create action)
#character = Character.new(params[:character]) #totally standard code
Notice that there's no mention of stats here at all. We specify the associations we want between characters and abilities and let rails handle the joins.
Railscasts episodes 196 and 197 show how to edit several models in one form. Example shown there looks similar to what you're trying to do so it might help you out (same episodes on ascicasts: 196, 197).

Resources