Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I cache the JSON representation of an object?

In the JSON representation of my Car model, I include the output of an expensive method:

#car.rb
def as_json(options={})
  super(options.merge(methods: [:some_expensive_method]))
end

I have a standard index action:

#cars_controller.rb
respond_to :json
def index
  respond_with(Car.all)
end

I also use the JSON representations of cars in other places, like this:

#user_feed.rb
def feed_contents
  Horse.all + Car.all
end

#user_feeds_controller.rb
respond_to :json
def index
  respond_with(UserFeed.feed_contents)
end

Because the JSON representation of a car is used in multiple places, I want it to be cached on its own, using car.cache_key as an auto-expiring cache key.

This is how I'm currently doing it:

#car.rb
def as_json(options={})
  Rails.cache.fetch("#{cache_key}/as_json") do
    super(options.merge(methods: [:some_expensive_method]))
  end
end

Putting the cache code inside as_json isn't correct though, because caching is not part of as_json's reponsibility. What is the proper way to do this? I'm using Rails 3.2.15.

like image 249
ben Avatar asked Nov 11 '13 19:11

ben


2 Answers

Let me first say I applaud the effort of this question. You will find it difficult to find someone more interested in designing software the right way--including with small methods doing one thing well at one level of abstraction--than I am.

However, I have to say that on this one I actually object to the premise of the question. I think your as_json is best the way it is.

What matters more than anything is decoupling clients from implementations. The only thing that clients of Car#as_json need to know is that the return value is the JSON representation of a Car. And as_json does that thing and does it well. The caching and/or fetching is an implementation detail that should stay inside the method, and it is a detail that is integral to that one task.

To say otherwise would be like saying any method with an if statement is "incorrect" because it does two things. Of course that isn't true. In both cases (using if and using Rails.cache.fetch), the method implementation is some atomic action whose outcome is based on a condition.

That's one thing that can go one of two ways, which is not the same as two things.

Meanwhile, I would have to disagree with @severin's response. It will work of course, but you have now coupled your view to an implementation detail. Nothing, and certainly not your view, should have any idea about the method of caching or even that caching is involved. In my opinion, you have now leaked an abstraction with this approach. Maybe it won't matter, but since we are talking about "the right way" to do things...

So I say keep things the way they are. But I believe it is a great question.

like image 84
Vidya Avatar answered Sep 21 '22 06:09

Vidya


I always put the caching into the as_json method (and the active_model_serializer gem does that as well), but your remark regarding this not being correct got me thinking and I can understand your concerns.

So I looked through the respond_with documentation (see enter link description here) and I found this:

If an acceptable format is not identified, the application returns a ‘406 - not acceptable’ status. Otherwise, the default response is to render a template named after the current action and the selected format, e.g. index.html.erb. If no template is available, the behavior depends on the selected format...

So you could create a json template for the affected actions and then do the caching in this view. Something like following should work:

# app/views/cars/index.json.erb
[<%= @cars.map {|car| render partial: 'cars/car.json', locals: {car: car}}.join(',') %>]

# app/views/cars/show.json.erb
<%= render partial: 'cars/car.json', locals: {car: @car} %>

# more templates for other actions...

# app/views/cars/_car.json.erb
<%= Rails.cache.fetch("#{car.cache_key}/as_json") { car.as_json } %>

You probably can clean up the render partial: ... calls a bit and then you would have a nice solution with the caching handled in the view where it belongs.

like image 24
severin Avatar answered Sep 21 '22 06:09

severin