Ecto remove preload
Asked Answered
J

6

6

Is there any way to do the inverse to preload?

%Post{
  comments: []
}

posts = Repo.all(Post) |> Repo.unload(:comments)

%Post{
  comments: #Ecto.Association.NotLoaded<association :comments is not loaded>,
}
Jubilate answered 24/4, 2018 at 8:15 Comment(5)
Well Repo.all(Post) will already have comments of each Post set to NotLoaded so I'm not sure what you want.Mcneal
In case you receive the Post previously loaded, is there any option to unload it without querying again from the DB?Jubilate
@Jubilate what would be the exact reason for doing that? Looks pretty much as an XY problem.Bekelja
Yes it sounds more as a XY problem but I was just wondering if there was a way to unload the association because I couldnt find anything here hexdocs.pm/ecto/Ecto.html The issue is I am receiving in a test an object which already has preloaded an association and I want to test it with a library which isnt preloading the association and I cannot assert post1 == post2 if just one of them has the comments preloadedJubilate
I can fix the inner library which isnt preloading but it just made me wonder if there is an inverse preload function for EctoJubilate
B
11

Ecto.Association.NotLoaded is a plain old simple struct, so you might relatively easy implement this unpreload youself:

defmodule Unpreloader do
  def forget(struct, field, cardinality \\ :one) do
    %{struct | 
      field => %Ecto.Association.NotLoaded{
        __field__: field,
        __owner__: struct.__struct__,
        __cardinality__: cardinality
      }
    }
  end
end

And use it later as:

Unpreloader.forget(%Post{....}, :comments)
Bekelja answered 24/4, 2018 at 9:19 Comment(3)
hey, thats cool for tests, when you have did insert() & build(:assoc) with ex_machine, and then comparing to the result of Item.get!()Culet
Elixir has an official way of doing this: Ecto.reset_fields/2 (I made an answer too, but commenting just for visibility).Flaming
@Flaming thanks, it’s ecto, not elixir who has an official way of doing that, and I believe it hasn’t back in 2018, thank you for heads up!Bekelja
M
5

Answering the actual question from comments:

The issue is I am receiving in a test an object which already has preloaded an association and I want to test it with a library which isnt preloading the association and I cannot assert post1 == post2 if just one of them has the comments preloaded

If everything else is the same, I'd just delete that field before asserting:

assert Map.delete(post1, :comments) == Map.delete(post2, :comments)

or if you want to delete more than one field:

fields = [:comments, :users]
assert Map.drop(post1, fields) == Map.drop(post2, fields)
Mcneal answered 24/4, 2018 at 9:26 Comment(0)
F
4

Elixir has an official way of doing this: Ecto.reset_fields/2.

It's actually mentioned under the Repo.preload/3 docs too.

Flaming answered 25/10, 2023 at 12:10 Comment(0)
S
3

Just wrote a cleaner solution to this today that can dynamically build the %Ecto.NotLoaded{} struct using Ecto's schema reflection:

defmodule UnPreloader do
  def clear_associations(%{__struct__: struct} = schema) do
    struct.__schema__(:associations)
    |> Enum.reduce(schema, fn association, schema ->
      %{schema | association => build_not_loaded(struct, association)}
    end)
  end

  defp build_not_loaded(struct, association) do
    %{
      cardinality: cardinality,
      field: field,
      owner: owner,
    } = struct.__schema__(:association, association)
    %Ecto.Association.NotLoaded{
      __cardinality__: cardinality,
      __field__: field,
      __owner__: owner,
    }
  end
end

In 2023 you should use Ecto.reset_fields/2 as mentioned in @krivar's answer: https://mcmap.net/q/1589721/-ecto-remove-preload

Single answered 14/9, 2018 at 1:37 Comment(0)
P
1

if you need to compare 2 structs in tests, it's possible to create a comment without preloaded post association by specifying post_id field directly:

post = insert!(:post)
comment = insert!(:comment, post_id: post.id)
# instead of
# comment = insert!(:comment, post: post)

or else if you don't need comments association in post, just create post and its comments separately:

post = insert!(:post)
comment = insert!(:comment, post_id: post.id)
# instead of
# post = insert!(:post, comments: [build(:comment)])
Puggree answered 24/12, 2018 at 10:49 Comment(0)
F
0

Here is implementation to deal with associations weather they are loaded or not. If for example Post has users and comments

result = Post |> preload(:comments)
UnPreloader.clear_associations(result)

output will preload comments and delete users

Implementation:

defmodule UnPreloader do
  require Logger

  @doc """
    When list is passed as parameter it will match call this function
  """
  def clear_associations(list) when is_list(list) do
    Enum.map(
      list,
      fn item -> clear_associations(item)
      end
    )
  end

  @doc """
    When struct is passed as parameter it will match call this function.

    We fetch all associations in struct and then call map_schema which will check if association is not loaded
  """
  def clear_associations(%{__struct__: struct} = schema) do
    associations = struct.__schema__(:associations)
    map_schema(schema, associations)
  end


  @doc """
    When nil is passed as parameter it will match call this function.
  """
  def clear_associations(nil = schema) do
    nil
  end

  @doc """
    When we call multiple associations this function is called and it replaces each association in schema with eather
    warning or actual data, depends if association is loaded.
  """
  defp map_schema(schema, associations) when length(associations) > 0 do
    associations
    |> Enum.reduce(
         schema,
         fn association, schema ->
           %{schema | association => map_assoc_data(Map.get(schema, association))}
         end
       )
  end

  @doc """
    If schema has 0 associations we dont need to do anything. aka recursion braker
  """
  defp map_schema(schema, associations) when length(associations) == 0 do
    schema
  end

  @doc """
    If schema is nil we just return nil
  """
  defp map_assoc_data(data) when data == nil do
    nil
  end

  @doc """
    If schema is actually our produced warning we will just return it back
  """
  defp map_assoc_data(%{warning: _} = data) do
    data
  end

  @doc """
    If schema is actually a list we want to clear each single item
  """
  defp map_assoc_data(associationData) when is_list(associationData) do
    Enum.map(
      associationData,
      fn data ->
        clear_associations(data)
      end
    )
  end

  @doc """
    If schema is not list and association is not loaded we will return warning
  """
  defp map_assoc_data(%{__struct__: struct} = schema)
       when struct == Ecto.Association.NotLoaded and is_list(schema) == false do
    Logger.warn("Warning data not preloaded #{inspect schema}")
    %{
      warning: "DATA NOT PRELOADED"
    }
  end

  @doc """
    If schema is not list and association is loaded we will go deeper into schema to search for associations inside
  which are not loaded
  """
  defp map_assoc_data(%{__struct__: struct} = schema)
       when struct != Ecto.Association.NotLoaded and is_list(schema) == false do
    clear_associations(schema)
  end
end
Felecia answered 4/10, 2018 at 18:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.