Phoenix contexts and data preloading

I’m building my first app using Phoenix, and I was wondering what’s the recommended convention or best practice around preloading associations before returning models. Do you think it’s something the caller should control through options in a keyword list? Something along the lines of:

MyApp.Accounts.get_user(id, preload: "oauth_account")

My fear is that we don’t preload from within the context we’ll end up with consumers of the context, usually modules under the MyAppWeb having a strong dependency with MyApp.Repo, which doesn’t seem like a good idea. Should we instead map the Ecto models into a plain object? In the case above it could be a MyApp.Accounts.AccountWithOauth struct.

1 Like

The people over at Dockyard do it like this: beacon/pages.ex at 823cfa135acd47bb740f1f6c513f91616a0e5a7a · BeaconCMS/beacon · GitHub so I think it’s a good way of doing it.

For more complex preloads you can always decide to rewrite the list in the context if needed.

2 Likes

My preference is:

Core.Accounts.get_user(id)
|> Core.Accounts.preload_user_oauth()

I like that it’s composable and more easily tested.

2 Likes

This assumes that you always want to preload certain relations right? Do you then have functions that get the user without preloading? Can you explain why your approach would be more easily tested?

edit: my assumption is that this lives in the context module, are you using it there or do you use it like this in your liveview / controllers?

1 Like

I like to do:

def list_stuff(opts \\ []) do
  preload = Keyword.get(opts, :preload, [])

  __MODULE__
  |> Repo.all()
  |> Repo.preload(preload)
end

…then you are open to add other options for sorting and whatnot.

1 Like

In my example, Core.Accounts is the context module. It would have functions that retrieve one or more schemas (eg Core.Accounts.get_user/1 and Core.Accounts.list_users/0) and then zero or more preload functions (Core.Accounts.preload_user_auth/1, Core.Accounts.preload_user_profile/1 etc.)

From other modules, such as a LiveView, you could call Core.Accounts.list_users() |> Core.Accounts.preload_user_profile() or some other combinations of schema-fetching and preloading functions.

It’s more easily testable because each of those functions can be tested separately (preloading one user’s profile doesn’t need to be tested separately from preloading multiple users’ profiles because you can trust that preload does this correctly).

1 Like

Thanks for the clarification, I like your solution and I think yours might even be better in the long run. Especially when you have more complex preloads.