Following.rb
belongs_to :show
def cached_show
Rails.cache.fetch([self, :show]) do
show
end
end
View:
<% #recently_favorited.each do |following| %>
<li>
<%= link_to "#{following.cached_show.name}", show_path(:permalink => following.cached_show.permalink) %> <span><%= "(#{pluralize(following.cached_show.followers, "follower")})" %></span>
</li>
<% end %>
Result in the console:
Cache read: followings/632770-20120929132253/show
Cache generate: followings/632770-20120929132253/show
Show Load (0.7ms) SELECT `shows`.* FROM `shows`WHERE `shows`.`id` = 617 LIMIT 1
Cache write: followings/632770-20120929132253/show
Cache read: followings/632770-20120929132253/show
Cache fetch_hit: followings/632770-20120929132253/show
Cache read: followings/632770-20120929132253/show
Cache fetch_hit: followings/632770-20120929132253/show
Question:
Is this even a "correct" implementation of fetching/caching an association?
And what about performance?
In some views (as in the example) it will hit the cache 3 times per loop. In my case I'm looping 10 items in the footer, so it will make 30 hits on every request. Is this fine, or will a single n+1 query per loop be better?
Advise and general best practices appreciated :)
Creating a distinct method to hit the cache vs getting it fresh is not common from what I can tell.
Most of the time, you'd just call a method that asks the cache all the time, since if you include an object in the cache-key, the updated_at field is used to build the key.
For your example now, the weird part is that you don't actually do anything with the Following model apart accessing its association. Therefore, you should query directly on the Show model :
#recently_favorited_shows = Show.joins(:followings).order("followings.created_at DESC").uniq
Then in your view, loop on the shows. Only one query, no n+1
If you expect thousands of hits then, I'd just suggest to cache the result of #recently_favorited_shows and expire it every X minutes :
#recently_favorited_shows = cache_store.fetch('recently_favorited_shows', expires_in: 5.minutes){Show.joins(:followings).order("followings.created_at DESC").uniq}
On another note, here's a good write-up on cache usage on the view side if you want to do it some time: http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works
No joins solution
Edit : now, if you have gazillions of rows in followings table, here's what I'd do :
Create a field last_followed_at on the shows table, with an index on it
In Following.rb : belongs_to :show, touch: :last_followed_at. This way, as soon as you add a new entry in Following, it'll update the field on the shows table
Then, to get the latest followed shows, do :
#shows = Show.order("last_followed_at DESC").limit(10) # Fast query of course
This doesn't answer my question, but it solves my problem. Here's how I'll do it instead:
#shows = Rails.cache.fetch("recently_favorited_shows", expires_in: 1.minutes) do
Show.find(Following.order("created_at DESC").limit(10).collect(&:show_id))
end
The queries are pretty fast (~0.8ms each says the IRB console)
Related
On posts index page I list all posts this way:
posts_controller.rb
def index
#posts = Post.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 %>
35 posts per page
When I first load the page in dev env,
rack-mini-profiler shows this time: 1441.1 ms
after a few reloads: ~700 ms
Can I somehow decrease this time and number of sql requests?
Here're rmp images if it helps:
You could decrease the number of sql queries by:
including user as well as comments, since you seem to be using that when displaying the gravatar
changing post.comments.count to post.comments.size
While size, count, length are synonymous for arrays, for active record relations or associations they are not the same:
length loads the association (unless it is already loaded) and returns the length of the array
count does a select count(*) query whether the association is loaded or not
size uses length if the association is loaded and count if not.
In your case the comments association is loaded, but because you are using count, it's not actually used
Further, you don't actually seem to be using the comments collection for anything other than printing the number of records. If that's indeed the case, use counter_cache (4.1.2.3) instead of querying for the comments (the number of comments will be available in the parent record Post).
Also consider a client side alternative to time_ago_in_words. It will also help if you later decide to cache the entire section/page.
And finally retrieve only the fields you're going to use. In this case, I can imagine the Post contains a large amount of text for the content and it's not used anywhere (but still needs to be transmitted from the DB).
Adding an index on the reference column (comments in your case) might help.
add_index :posts, :comment_id
I have a Rails app that pulls in music from Soundcloud. This data contains a title, which I save as mix.sc_title but it's not always properly formatted. I have added an additional attribute on my Mix model which I call mix.override_title
For display on my site, I want to use the override title if available, and the sc_title in all other cases.
I have a Mix model method to do this for me
def display_title
override_title.blank? sc_title : override_title
end
Mixes#index grabs #mixes = Mix.where(:active => true) and mixes/index.html.erb looks like this:
<ul>
<% #mixes.each do |mix| %>
<li><%= link_to mix.display_title, mix %></li>
<% end %>
</ul>
As you can see, I'm not directly using any mix attributes, and so I take a huge hit when I go to the DB, and I don't actually benefit from it.
Is there a leaner way to get just the information I need? (mix.display_title)
I have tried Mix.select("display_title").where(:active => true) but it fails because display_title is not a real DB column
You can do Mix.select("sc_title, override_title").where(:active => true) and it will work, since those are the actual fields that the method uses. I don't really think getting the additional attributes gives you that much of a DB hit but sometimes selecting only what you need can be beneficial.
As you start chaining on more Arel commands, consider putting the select into a model method:
def select_active_titles
select("sc_title, override_title").where(:active => true)
end
Edit: Your link_to helper also secretly calls mix.id to link to the right mix, so make sure it's working and if not add id to the list of selected attributes.
I have a controller action that calls a model method which generates a serialized list of data pulled from another model database. I need this to be uncached because the SQL queries should be random data pulls.
Here's a general idea of my code (Note that User has_one Foo, Bar is an arbitrary model of data, :data_list is of type text, and the database is SQLite):
# app/models/foo.rb
class Foo < ActiveRecord::Base
serialize :data_list
def generate_data
list = []
for i in 1..4
data = Bar.find(:first, :order => "Random()")
list << data
end
self.data_list = list
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def generate_action
...
uncached do
#user.foo.generate_data
end
#user.foo.save
end
end
# app/views/user/show.html.erb
...
<% #user.foo.data_list.each do |data| %>
<%= data %><br />
<% end %>
Whenever uncached do ... end is removed, everything works fine and the show view prints out each set of Bar objects in #user.foo.data_list. Unfortunately, because of Rails' SQL caching, it ends up look like this:
RandomDataPoint8
RandomDataPoint8
RandomDataPoint8
RandomDataPoint8
When I need to look like this:
RandomDataPoint7
RandomDataPoint13
RandomDataPoint2
RandomDataPoint21
It should be noted that running user.foo.generate_data from Rails command line works perfectly with the randomization. It is only when being called from the controller that caching starts to occur.
My research suggested I use uncached in the controller to remove caching, however it seems to destroy my data serialization and I receive the error:
undefined method 'each' for #<String:0x007ff49008dc70>
In fact, it does this even if I retroactively add in uncached (having successfully generated a data_plan without uncached prior) and save the controller, but don't call generate_action.
EDIT
I believe this problem is actually related to the fact that I was storing an object in the hash. Switching to the object id fixed this problem. Another SO question of mine regarding this can be found here:
Rails - Accessing serialized data from console
The following has been preserved just because the syntax may still help people, but I don't believe it was the actual cause of the problem.
I solved this by moving uncached to the model. For reference, the source I was using to originally solve this problem was this link: http://railspikes.com/2008/8/18/disabling-activerecord-query-caching-when-needed
What I overlooked is that he puts uncached in the model, not the controller. Also, the syntax needed to be a little different:
# app/models/foo.rb
self.class.uncached do
...
end
instead of
uncached do
...
end
The source for the syntax correction is this SO response: https://stackoverflow.com/a/967690/337903
Lets say you have a post with comments on the same page, and you render a form for capturing a new comment also on the same page as you are displaying the post/comments. A post has_many comments. Code as follows:
class PostsController < ApplicationController
...
def show
#post = Post.find(:params[id])
#comment = Post.comments.new
end
...
end
Now when you call <%= #post.comments.count %> in your views it gives the number of comments that have been saved, but if you call <%= render #post.comments %> it returns all the saved comments PLUS the newly created (but not yet saved and therefore still invalid) comment. Why is this? This has really taken me time to find this and I can't imagine that this would be useful, why not just render all the valid database records?
Has anyone else ran into this? Easy to fix but puzzling..
Well, #post.comments.count actually does a database query and can therefore only return the number of saved records. (Use #post.comments.size or .length) for the number of objects in your collection.
The render call, AFAIK, only loops over the objects in the collection.
The thing to know here is the difference between when you do actual queries with the association, and when active record is using the cached objects. It is perhaps easy to assume that the comments in #post.comments is just an Array. It actually is a fancy proxy object that, depending on method called and state of the cached collection, acts like an Array or as an interface to the Model's query methods.
so i want to make a page the displays a Phrase by select (initially) at random from the database. on that page i want a <%= link_to "next"%> but i was wondering if there was an efficient way to ensure that the next record exists
currently I'm using just
# #phrase is current phrase
<%= link_to "next", phrase_path( Phrase.find( #phrase.id + 1 ) ) %>
yes, i know i should call a #next from the controller, or better yet have a next method in the model to call #phrase.next, but this is for illustrative purposes.
but this often turns up an ActiveRecord::RecordNotFound error because some phrases have been deleted from the db (due to moderation, error, etc...). I could rescue from this and loop that till it works in the controller then pass it or something, but that seems like a bad solution, and not particularly 'railsy'
is there a convenient solution to this anyone has found
figured it out
based on this link which is a little outdated, uses named_scope from back in rails 2. I first rewrote it using the new rails 3 scope style, but then just changed it to a method. just used
def next
Phrase.where("id > ?", self.id).order("id ASC").first
end
def previous
Phrase.where("id < ?", self.id).order("id DESC").first
end
Try creating a next/previous scope on your model, as suggested in http://steve.dynedge.co.uk/2010/01/13/random-previous-and-next-entries-from-active-record-models-using-offset/
This will allow you to do something like:
Phrase.next(5) or Phrase.next(#phrase.id)
Why don't you create a method in the controller called next and pass in the current record id. It would be trivial from there to redirect the user back to the show page for that next resource.
If you are deadset on creating the link in advance, look into creating a helper method to find the next record that exists and make it available in your views. Then you could call that whenever you needed the id of the next available record.
Something like will_paginate might be of help too. I know your page size is just one, but the essence of what you're doing is pagination.