Ecto Preloading nested associations with computed fields

Hi :wave:

I am trying to get my head around this problem for a couple of days but I can’t find an elegant solution. I’d love to get your help on this.

Here is a simplification of my data model:

  • A Recipe has many Versions
  • A Version has many Ingredients
  • A Version has many Media

I have a domain context called “Kitchen” that I use to retrieve/delete/create/update recipes.

I have implemented 2 functions on it: get_recipe and get_recipe_history. The first simply retrieves a recipe, the second preloads, for a given recipe, all versions and for each, all ingredients & media.

My problem is that the Media schema has some computed fields that can’t be inferred with the database data alone. I need to call a method on my Media secondary context to populate them. (Those fields represents a public URL of the media.upload stored in database, it uses some mix config values to generate them).

My code is currently:

  def get_recipe_history(recipe_or_recipes) do
    Repo.preload(recipe_or_recipes,
      recipe_versions: [:media, :ingredients]
    )
  end

but this doesn’t populate what I need on Media structs.

I have therefore added this function, that any callers need to invoke before accessing the public URL of the media:

  defp maybe_populate_upload_urls(media) when is_list(media) do
    Enum.map(media, &maybe_populate_upload_urls/1)
  end

  defp maybe_populate_upload_urls(%Media{} = media) do
    ...
  end

It works, but I find it quite inconvenient. I’d like to automatically provide to any callers the public URL of a Media.

On the other hand, I have a Media secondary context with this code:

  def all(%RecipeVersion{} = version, preload \\ []) do
    Repo.all(assoc(version, :media))
    |> maybe_populate_upload_urls()
    |> Repo.preload(preload)
  end

which works perfectly but I can’t see how to take advantage of that in my Kitchen context.

I hope all of this makes sense. I found it really difficult to find an elegant pattern to do data preloading with ecto. Your help would be really appreciated.

Thanks in advance. :blush:

I actually found my answer after remembering “preloader functions” from Ecto docs.

This doc helped me resolve my problem.

I am now calling something like this from the Kitchen context. Recipes, RecipeVersions, Medias are all secondary context.

Recipes.all(user, [recipe_versions: { &RecipeVersions.last/1, [media: &Medias.all/1] }])

The Media preloader has the responsibility of populating computed fields by running this (Media is a schema):

    Media.all(version_ids)
    |> Repo.all()
    |> maybe_populate_upload_urls()

Thanks for having taking the time to read my post.

2 Likes