Ecto 去除预紧力

Ecto remove preload

有什么方法可以做相反的预加载吗?

%Post{
  comments: []
}

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

%Post{
  comments: #Ecto.Association.NotLoaded<association :comments is not loaded>,
}

Ecto.Association.NotLoaded 是一个普通的老式简单结构,因此您自己实现这个 unpreload 可能相对容易:

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

稍后用作:

Unpreloader.forget(%Post{....}, :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

如果其他一切都一样,我会在断言之前删除该字段:

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

或者如果您要删除多个字段:

fields = [:comments, :users]
assert Map.drop(post1, fields) == Map.drop(post2, fields)

今天刚刚为此编写了一个更简洁的解决方案,可以使用 Ecto's schema reflection:

动态构建 %Ecto.NotLoaded{} 结构
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

这里是处理关联是否加载的实现。 例如,如果 Post 有用户和评论

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

输出将预加载评论并删除用户

实施:

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

如果您需要在测试中比较 2 个结构,可以通过直接指定 post_id 字段来创建没有预加载 post 关联的评论:

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

否则,如果您不需要 post 中的 comments 关联,只需单独创建 post 及其评论:

post = insert!(:post)
comment = insert!(:comment, post_id: post.id)
# instead of
# post = insert!(:post, comments: [build(:comment)])