Ecto remove preload

随声附和 提交于 2019-12-19 09:37:36

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!