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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With