Recently I was updating an older Phoenix app that was built when I was first familiarizing myself with Phoenix and realized there were a couple architectural mistakes in some of the controllers and views. I rectified those mistakes with the following patterns.
Keep your database out of my view by preloading
The view layer in Phoenix serves as a decorator for the template you are about to render. It serves to merge data retrieved in the controller with the additional details that may be needed. On our JSON API backends, we use ja_serializer to convert our models into the proper response. When we have additional details (say we include a blog post’s author for side loading into Ember), we want to make sure that we preload this data before it gets to the view. If we preload from the view itself, we will be creating an N+1 scenario.
Let’s take the case of an index route where you have 20 posts you want
to render. When we wait until we get to the view’s
renders the individual post) to retrieve the author, we will be doing 20
additional SQL queries, as we will be getting the post one by one. If we
preload the author relationship in the controller, we will be executing
a single query that retrieves all the authors and places them in the
struct with the post information.
Keep your decoration out of your controllers
You may have additional information you want your view to have access
to. In one case that I came across, we query the database to find the
number of blog posts a specific tag has. We end up storing the
information in a map in the function we created to look up that
information, where the key is the id of the tag, and the value is the
number of posts for that tag. We just went over why you don’t want to do
this work in your view, so how do we pass that information down? We can
Plug.Conn.assign (docs) to store the
information on the
conn which is passed to render in the controller.
What we don’t want to do is start
Enum.maping across the list of tags
and munging the data in the controller. That would be decorating the
model, which is the job of the view.
Everything in its right place
By following the practice of decorating in the view, and doing data
retrieval in the controller, it actually makes it quite difficult to
create an N+1 query. When you have a list of records, the easiest way to
preload data (if you aren’t using
Ecto.Query.preload), is to call
Repo.preload(collection, keys), which will only perform a single query
in the end.
Moving from Rails to Phoenix, I’ve personally found there to be fewer footguns to shoot myself with. I think a big part of that ends up being the fact that with a functional language, you can’t accidentally make a call to the database because your relationship can’t load itself. With Ecto, I have to explicitly load the data for the relationships and that requires me to think about when is the best point to do so.