Preloaded associations and views patterns

I’m wondering how do people structure their JSON Api’s with Phoenix. Using the blogs example, let’s say I have a blogs view like the following

  def render("show.json", %{blog: blog}) do
    %{
      id: blog.id,
      title: blog.title,
      posts: render_many(blog.posts, PostView, "post.json")
    }

This runs into things like what if posts has its own associations like comments, etc.

In my controller maybe sometimes I don’t care about the associations and other times I do. This leads me to two main problems?

  1. Are there any standards people follow to handle when a view should try and render associations?
  2. Should there be some helper function in Phoenix.View that handles conditionally rendering associations?

For the first problem I’m just curious how people do something like the following

  def show(conn, %{"id" => id}) do
    blog = Blogs.get_blog_with_all(id)
    # assume no associations are preloaded
    render(conn, "show.json", blog: blog)
  end

  def some_action(conn, %{"id" => id}) do
    blog = Blogs.get_blog_with_all(id, preload: [:association_one, :association_two])
    # here I want to include the two associations in my view
    render(conn, "show_with_association_one_and_two.json", blog: blog)
  end

Having a specific view for each combination of association seems tedious.

For the second problem I mean something like having a helper function maybe_render_association that will add the field to the view if the association is loaded or it will not include the field at all. My thinking is we don’t want to return nil in the case that the association is not loaded since it’s not actually nil. That pattern would look like


def render() do
  def render("post.json", %{post: post}) do
    %{
      id: post.id,
      title: post.title,
      body: post.body,
      comments: render_many(post.comments, CommentView, "comment.json")
    }
    # it would be nice if we can have it inside the above map but 
    # since it's a conditional field not sure how
    |> maybe_render_association(post.updated_by, :updated_by, User, "basic.json")
  end
end

I never generalize those views.
I keep them specific to the action that renders them.
This keeps the information together and prevents unneeded data going to the client.

The a look at the JSONAPI spec which has “includes” for this.

Be careful though that you take care of proper visibility checks even if you are including a relationship.

Ah I didn’t know that that was part of the spec, that is nice to know.

I can see that. Do you have a particular way to structure that? For example let’s say we have two actions list_blogs and get_blog that our endpoints use. Let’s say that list_blogs doesn’t want to preload created_by but get_blog does. How would you name the templates for the JSON view?

So the controller will delegate out to the view

  # controller
  def list_blogs(conn, params) do
    ...
    render(conn, "index.json", blogs: blogs)
  end

  def show(conn, %{"id" => id}) do
    ...
    render(conn, "show.json", blog: blog)
  end

We can assume that the controller properly preloads the data it needs. Then in the View how do you handle this conditional logic of some fields were preloaded and therefore exist but for other templates they don’t?

  # view file
    def render("index.json", %{blogs: blogs}) do
    %{data: render_many(blogs, __MODULE__, "NO CREATED BY PRELOAD")}
  end

  def render("show.json", %{blog: blog}) do
    %{data: render_one(blog, __MODULE__, "NEEDS CREATED BY PRELOAD.json")}
  end

So far I’ve referencing the same template in both render_many and render_one cases. So I’d call something like "blog.json" but then I run into the case that index doesn’t want to show the preloaded data but show does

Imo you’re at the “consequences” end of a decision, but you haven’t talked about the decision itself: Why do you need that in the first place? What is the reason for representing the same data in distinct ways. That reason is your key to splitting up the codepaths.

I use the newer syntax, so no longer render_many or render_one, but just returning the map.

def render("index.json", %{blogs: blogs}) do
  for blog <- blogs do
    %{
      id: blog.id,
      title: blog.title
    }
  end
end

def render("show.json", %{blog: blog}) do
  %{
    id: blog.id,
    title: blog.title,
    created_by: blog.who,
    created_at: DateTime.to_is8601!(blog.inserted_at)
  }
end

I believe the reason is that I want a flexible way to preload data which causes some fields to be sometimes present. To present that as a template I think the main two ways are

  1. To have one view template to rule them all
  2. To have one view template per distinct way

The first one sounds better but then I’d need to introduce some utilities to handle the conditional logic which leads me to this type of pattern to handle the fact that if an association is not preloaded, then I don’t want it to show up at all in the response (I don’t want to return nil for a case where we haven’t loaded the data)

def render() do
  def render("post.json", %{post: post}) do
    %{
      id: post.id,
      title: post.title,
      body: post.body,
      comments: render_many(post.comments, CommentView, "comment.json")
    }
    # it would be nice if we can have it inside the above map but 
    # since it's a conditional field not sure how
    |> maybe_render_association(post.updated_by, :updated_by, User, "basic.json")
  end
end

If there’s no better way and these utilities are overall the best approach then I was thinking that maybe it should be built into Phoenix.View

Ah true I guess render_many and render_one are not really needed anymore. I guess in your case how would associated data use these views?

So if a user has many posts and you want to show that for the “user.json” render do you just use another for loop or do you try and refer to the blog views?

Mine usually is an index view that lists all “items”, then a show that takes the id and returns the details for that item, this is in an app where the frontend is in Elm.

And I use distinct views for each. There is seldom any reuse as each page required other data and gets only that data.

I feel that if you want your frontend to dictate what data should be returned, you look into GraphQL.

I don’t think phoenix should drive such helpers. I’d argue a none preloaded association reaching a view template expecting that association to be preloaded is an error. There’s no definitive answer to if that association is missing because of a bug, because of the intention to skip the key of the association in the json or because of the intention to mask the data not being loaded with an empty value ([] or nil). I’ve seen people argue in favor of all of these, so a generalize solution is likely not the answer.

You can however quite build such a helper for your project and your intentions.

Personally I’d push this concern to other places though. E.g. have PostUser – like a user as shown in relation to posts – vs a UserDetails – user as rendered by e.g. /users/:id. That requires more upfront modeling, but makes code a lot more explicit. You’re not passing around data, which could fit 10+ branches of results – and you hope you get the one you intend – but you pass around data that matches the one or few intented usecases. It also makes change easier, because the approach of a single very dynamic view template works great if all “branches” are kind of supersets of the base case. It gets hairy though once they have conflicting keys. Say eventually you want to mask part of the users as returned with posts, but not mask that for user details. It might feel like duplication at first, but going that route is more maintainable in the long run.

2 Likes