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>,
}
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>,
}
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)
Item.get!()
–
Culet 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)
Elixir has an official way of doing this: Ecto.reset_fields/2.
It's actually mentioned under the Repo.preload/3 docs too.
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
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)])
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
© 2022 - 2024 — McMap. All rights reserved.
Repo.all(Post)
will already have comments of each Post set toNotLoaded
so I'm not sure what you want. – Mcneal