问题
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>,
}
回答1:
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)
回答2:
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)
回答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
回答4:
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
回答5:
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)])
来源:https://stackoverflow.com/questions/49996642/ecto-remove-preload