The query part can be accomplished quite simply with a scope:
class Category
scope :grouped_entries, lambda { |user, date|
select(['categories.*', 'COUNT(entries.id) as count_entries'])
.joins(:entries)
.where('entries.user_id = ?', user.id)
.where('MONTH(entries.created_at) = ?', date.month)
.group('categories.id')
}
end
Which can then be looped over:
<% Category.grouped_entries(current_user, Date.today).each do |category| %>
<%= category.name %> with <%= category.count_entries %> entries this month.
<% end %>
Of course, caching this requires that you refresh the cache anytime an entry is created this month. For example, you could cache the query like this:
@categories = Rails.cache.fetch("/grouped_entries/#{current_user.id}/#{Date.today.month}") do
Category.grouped_entries(current_user, Date.today).all
end
And then simply expire it when a new entry is created by using the user_id and the entry created_at month. I would say that you should use this approach first, before trying to cache every single category's entries individually. This query should perform quite quickly, so you shouldn't have to delve into caching each row individually. It will also perform a single query rather than one for each category.
Here is why I wouldn't cache each row individually:
- You still have to query the database to get a list of categories or category ids for a user, so you have to perform one query anyways.
- Cachine expiration is more complicated because there are more cases where you have to expire two caches, for example when a category for an entry changes you have to expire the old category cache and the new category cache.
- You may end up running more queries against your database to grab expired cache information and the latency to the database will probably end up taking longer than the actual query.
- You don't need to cache every row because the query is simple and uses indexes. You should have an index on user_id and category_id for entries.