diff --git a/lib/ecto/repo.ex b/lib/ecto/repo.ex index 54c3588a18..0b63e69f20 100644 --- a/lib/ecto/repo.ex +++ b/lib/ecto/repo.ex @@ -1188,6 +1188,10 @@ defmodule Ecto.Repo do * `:on_preloader_spawn` - when preloads are done in parallel, this function will be called in the processes that perform the preloads. This can be useful for context propagation for traces. + * `:warn_if_nil_keys` - When set to `false`, suppresses the warning + when preloading associations where the parent's association key is nil. + Useful when working with unpersisted data (e.g., in changesets). + Defaults to `true`. See the ["Shared options"](#module-shared-options) section at the module documentation for more options. diff --git a/lib/ecto/repo/preloader.ex b/lib/ecto/repo/preloader.ex index 376decc0b5..d918df00ca 100644 --- a/lib/ecto/repo/preloader.ex +++ b/lib/ecto/repo/preloader.ex @@ -251,38 +251,46 @@ defmodule Ecto.Repo.Preloader do defp fetch_ids(structs, module, assoc, {_adapter_meta, opts}) do %{field: field, owner_key: owner_key, cardinality: card} = assoc force? = Keyword.get(opts, :force, false) + warn_if_nil_keys? = Keyword.get(opts, :warn_if_nil_keys, true) - Enum.reduce structs, {[], [], []}, fn + Enum.reduce(structs, {[], [], []}, fn nil, acc -> acc + struct, {fetch_ids, loaded_ids, loaded_structs} -> assert_struct!(module, struct) %{^owner_key => id, ^field => value} = struct loaded? = Ecto.assoc_loaded?(value) and not force? - if loaded? and is_nil(id) and not Ecto.Changeset.Relation.empty?(assoc, value) do - Logger.warning """ + if loaded? and is_nil(id) and not Ecto.Changeset.Relation.empty?(assoc, value) and warn_if_nil_keys? do + Logger.warning(""" association `#{field}` for `#{inspect(module)}` has a loaded value but \ its association key `#{owner_key}` is nil. This usually means one of: * `#{owner_key}` was not selected in a query * the struct was set with default values for `#{field}` which now you want to override - If this is intentional, set force: true to disable this warning - """ + If you want to override the data, set force: true. + + If you are intentionally preloading data that has not yet been committed (e.g. a new struct + with id: nil), you can set warn_if_nil_keys: false to disable this warning. + """) end cond do card == :one and loaded? -> {fetch_ids, [id | loaded_ids], [value | loaded_structs]} + card == :many and loaded? -> {fetch_ids, [{id, length(value)} | loaded_ids], value ++ loaded_structs} + is_nil(id) -> {fetch_ids, loaded_ids, loaded_structs} + true -> {[id | fetch_ids], loaded_ids, loaded_structs} end - end + end) end defp fetch_query(ids, assoc, _repo_name, query, _prefix, related_key, _take, _tuplet) diff --git a/test/ecto/repo_test.exs b/test/ecto/repo_test.exs index 56fd4be402..1a9fd797ee 100644 --- a/test/ecto/repo_test.exs +++ b/test/ecto/repo_test.exs @@ -49,7 +49,7 @@ defmodule Ecto.RepoTest do schema "my_schema_child" do field :a, :string - belongs_to :my_schema, MySchema + belongs_to :my_schema, Ecto.RepoTest.MySchema belongs_to :my_schema_no_pk, MySchemaNoPK, references: :n, foreign_key: :n end @@ -2007,6 +2007,80 @@ defmodule Ecto.RepoTest do TestRepo.preload(%MySchema{id: 1}, children: intersect_all(query, ^query)) end end + + test "preload assigns belongs_to assoc" do + struct = %MySchemaWithAssoc{ + id: 1, + parent_id: 1 + } + + assert %Ecto.Association.NotLoaded{} = struct.parent + + mock_results = { + 1, + [ + [1, 2] + ] + } + + Process.put(:test_repo_all_results, mock_results) + + result = TestRepo.preload(struct, :parent) + + assert_received {:all, _query} + + assert result.parent == %MyParent{n: 2} + end + + test "preload assigns nested has_many->belongs_to assocs" do + structs = [ + %MySchema{ + id: nil, + children: [ + %MySchemaChild{ + id: nil, + my_schema_id: 123, + my_schema: %Ecto.Association.NotLoaded{} + } + ] + }, + %MySchema{ + id: nil, + children: [ + %MySchemaChild{ + id: nil, + my_schema_id: 456, + my_schema: %Ecto.Association.NotLoaded{} + } + ] + } + ] + + {result, _log} = + ExUnit.CaptureLog.with_log(fn -> + TestRepo.preload(structs, children: :my_schema) + end) + + assert [ + %{ + children: [ + %{ + my_schema_id: 123, + my_schema: %{id: 123} + } + ] + }, + %{ + children: [ + %{ + my_schema_id: 456, + my_schema: %{id: 456} + } + ] + } + ] = + result + end end describe "checkout" do