Globally Reuse ActiveRecord Queries across Models?
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:
- Rendering partials using
:collection
- 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?
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
精彩评论