Question

I've come to appreciate the "skinny controllers" philosophy in Rails that says that business logic should not be in controllers, but that they should basically only be responsible for calling a few model methods and then deciding what to render/redirect. Pushing business logic into the model (or elsewhere) keeps action methods clean (and avoids stubbing long chains of ActiveRecord methods in functional tests of controllers).

Most cases I've run across are like this: I have three models, Foo, Bar, and Baz. Each of them has a method or scope defined (call it filter) that narrows down the objects to what I'm looking for. A skinny action method might look like:

def index
  @foos = Foo.filter
  @bars = Bar.filter
  @bazs = Baz.filter
end

However, I've run into a case where the view needs to display a more hierarchical data structure. For example, Foo has_many bars and Bar has_many bazs. In the view (a general "dashboard" page), I'm going to display something like this, where each foo, bar, and baz has been filtered down with some criteria (e.g. for each level I only want to show active ones):

Foo1 - Bar1 (Baz1, Baz2)
       Bar2 (Baz3, Baz4)
-----------------------
Foo2 - Bar3 (Baz5, Baz6)
       Bar4 (Baz7, Baz8)

To provide the view with the data it needs, my initial thought is to put something crazy like this in the controller:

def index
  @data = Foo.filter.each_with_object({}) do |foo, hash|
    hash[foo] = foo.bars.filter.each_with_object({}) do |bar, hash2|
      hash2[bar] = bar.bazs.filter
    end
  end
end

I could push that down to the Foo model, but that's not much better. This doesn't seem like a complex data structure that merits factoring out into a separate non-ActiveRecord model or something like that, it's just fetching some foos and their bars and their bazs with a very simple filter applied at each step.

What is the best practice for passing hierarchical data like this from a controller to a view?

Was it helpful?

Solution

You could get @foos like this:

@foos = Foo.filter.includes(:bars, :bazs).merge(Bar.filter).merge(Baz.filter).references(:bars, :bazs)

Now your relation is filtered and eager loaded. The rest of what you want to do is a concern of how you want it presented in the view. Maybe you'd do something like this:

<% Foo.each do |foo| %>
  <%= foo.name %>
  <% foo.bars.each do |bar| %>
    <%= bar.name %>
    <% bar.bazs.each do |baz| %>
      <%= baz.name %>
    <% end %>
  <% end %>
<% end %>

Any kind of hash-building in the controller is unnecessary. The level of abstraction you're working with in the view is reasonable.

OTHER TIPS

One of the widely accepted best practices for this kind of thing if Extracting a Form object. Bryan Helmkamp from Code Climate has written a very good blog post about this:

http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

See section three "Extract Form Objects".

The thing is, yes, you should definitely move business logic out of your controllers. But it does not belong in your data models (Activerecord models) either. You will want to use a combniation of number 2, "Extract Service Objects", and 3, "Extract Form Objects", in order to build a good structure for your app.

You can also watch a good video of Bryan explaining these concepts here: http://www.youtube.com/watch?v=5yX6ADjyqyE

For more on these topics, it is also highly recommended that you watch Confreaks conference videos: http://www.confreaks.com/

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top