views:

26

answers:

1

Not sure how to word the question concisely :).

I have say 20 Posts per page, and each Post has 3-5 tags. If I show all the tags in the sidebar (Tag.all.each...), then is there any way to have a call to post.tags not query the database and just use the tags found from Tag.all?

class Post < ActiveRecord::Base

end

class Tag < ActiveRecord::Base

end

This is how you might use it:

# posts_controller.rb
posts = Post.all

# posts/index.html.haml
- posts.each do |post|
  render :partial => "posts/item", :locals => {:post => post}

# posts/_item.html.haml
- post.tags.each do |tag|
  %li
    %a{:href => "/tags/#{tag.name}"}= tag.name.titleize

I know about the following optimizations already:

  1. Rendering partials using :collection
  2. Eager loading with Post.first(:include => [:tags])

I'm wondering though, if I use Tag.all in my shared/_sidebar.haml template, is there anyway to reuse the result from that query in the post.tags calls?

+1  A: 

You can use the tag_ids method on a Post instance.

In your controller create the tag hash. Better still cache the tag hash.

Add this to your application_controller.rb.

def all_tags
  @all_tags ||=Rails.cache.fetch('Tag.all', :expire_in => 15.minutes)){ Tag.all }
  # without caching
  #@all_tags ||= Tag.all
end


def all_tags_hash
  @all_tags_hash ||= all_tags.inject({}){|hash, tag| hash[tag.id]=tag;hash}
end

def all_tags_by_ids ids
  ids ||= []
  ids = ids.split(",").map{|str| str.to_i} if ids.is_a?(string)
  all_tags_hash.values_at(*ids)
end
helper_method :all_tags, :all_tags_hash, :all_tags_by_id

Now your partial can be rewritten as

# posts/_item.html.haml
- all_tags_by_ids(post.tag_ids).each do |tag|
  %li
    %a{:href => "/tags/#{tag.name}"}= tag.name.titleize

My solution caches the Tag models for 15 minutes. Make sure you add an observer/filter on Tag model to invalidate/update the cache during create/update/delete.

In your config\environment.rb

config.active_record.observers = :tag_observer

Add a tag_observer.rb file to your app\models directory.

class TagObserver < ActiveRecord::Observer

  def after_save(tag)
    update_tags_cache(tag)
  end

  def after_destroy(tag)
    update_tags_cache(tag, false)
  end

  def update_tags_cache(tag, update=true)
    tags = Rails.cache.fetch('Tag.all') || []
    tags.delete_if{|t| t.id == tag.id}
    tags << tag if update
    Rails.cache.write('Tag.all', tags, :expire_in => 15.minutes)
  end
end

Note: Same solution will work with out the cache also.

This solution still requires you to query the tags table for Tag ids. You can further optimize by storing tag ids as a comma separated string in the Post model(apart from storing it in post_tags table).

class Post < ActiveRecord::Base
  has_many :post_tags
  has_many :tags, :through => :post_tags

  # add a new string column called tag_ids_str to the `posts` table.
end

class PostTag < ActiveRecord::Base
  belongs_to :post
  belongs_to :tag
  after_save :update_tag_ids_str
  after_destroy :update_tag_ids_str

  def update_tag_ids_str    
    post.tag_ids_str = post.tag_ids.join(",")
    post.save
  end
end

class Tag < ActiveRecord::Base
  has_many :post_tags
  has_many :posts, :through => :post_tags
end

Now your partial can be rewritten as

# posts/_item.html.haml
- all_tags_by_ids(post.tag_ids_str).each do |tag|
  %li
    %a{:href => "/tags/#{tag.name}"}= tag.name.titleize
KandadaBoggu
I updated the caching logic slightly, take a look.
KandadaBoggu