Preload association inside Embedded Schema from Schema level

Hi, i’m struggling to find a way to preload an association inside a embedded schema from the base schema level. Below is an example of what i’m trying to achieve:

defmodule TestAssoc do
  use Ecto.Schema

  schema "test_assoc" do
    field :name, :string
  end
end

defmodule TestEmbedded do
  use Ecto.Schema

  alias TestAssoc

  embedded_schema do
    belongs_to :test_assoc, TestAssoc
  end
end

defmodule Test.Repo do
  use Ecto.Repo,
    otp_app: :test,
    adapter: Ecto.Adapters.Postgres
end

defmodule Test do
  use Ecto.Schema

  alias TestEmbedded

  alias Test.Repo

  schema "test" do
    embeds_one :test_embedded, TestEmbedded
  end

  defp load_test do
    %Test{}
    |> Repo.all()
    |> Repo.preload({:test_embedded, [:test_assoc]})
  end
end

Is there any way to do this with embedded schemas?

Thanks in advance.

This PR suggests it should definitely be possible to preload test_assoc on a single TestEmbedded struct:

In the case you’re looking for in load_test, I’d expect this might work:

%Test{}
|> Repo.all()
|> Repo.preload(test_embedded: :test_assoc)

In the same way you’d preload a has_one and an association on that record.

I’ve seen a few people ask for that in the last handful of weeks. I’m really curious about the usecase behind that. Embeds are not bound by foreign key constraints, so this feels like something bound to get out of consistency.

1 Like

I’ve tried this method, but it throws the exception below:

** (ArgumentError) schema Test does not have association :test_embedded

Are you on a recent enough ecto version (granted I don’t know if that PR is already in a released version)?

I’m on ecto 3.5.5

  • ecto 3.5.5 (Hex package) (mix)
    locked at 3.5.5 (ecto) 98dd0e5e
    ok

At the moment I did a helper to do the recursive preload with fields that have embedded.
Is it okay to do this?

defmodule Test.Repo do
  use Ecto.Repo,
    otp_app: :test,
    adapter: Ecto.Adapters.Postgres

  def preload_with_embedded(struct_structs_or_nil, preloads, opts \\ [])

  def preload_with_embedded(structs, preloads, opts) when is_list(structs) do
    Enum.map(structs, fn struct ->
      struct |> preload_with_embedded(preloads, opts)
    end)
  end

  def preload_with_embedded(struct, preload, opts) when is_map(struct) and is_tuple(preload) do
    {field, nested_preloads} = preload

    struct_scope = Map.get(struct, field, nil)

    if Ecto.assoc_loaded?(struct_scope) do
      preload_with_embedded_handler(struct, field, nested_preloads, opts)
    else
      struct
      |> preload(field, opts)
      |> preload_with_embedded_handler(field, nested_preloads, opts)
    end
  end

  def preload_with_embedded(struct, preloads, _opts) when is_map(struct) and is_list(preloads) do
    Enum.reduce(preloads, struct, fn p, acc ->
      acc |> preload_with_embedded(p)
    end)
  end

  def preload_with_embedded(struct, preload, opts) when is_map(struct) and is_atom(preload) do
    struct |> preload(preload, opts)
  end

  defp preload_with_embedded_handler(struct, field, nested_preloads, opts) do
    case Map.get(struct, field, nil) do
      ss when is_nil(ss) ->
        nil

      ss when is_map(ss) ->
        Map.put(
          struct,
          field,
          preload_with_embedded(ss, nested_preloads, opts)
        )

      ss when is_list(ss) ->
        Map.put(
          struct,
          field,
          Enum.map(ss, fn nested_struct ->
            nested_struct |> preload_with_embedded(nested_preloads, opts)
          end)
        )

      _ ->
        nil
    end
  end
end

Here’s one use case: (I think) Using embedded schema ad a final staging check to validate egress data that is going to a 3rd party data consumer. Once you perform the validation, you shoot it off to the 3rd party api and never see it again.

1 Like