assoc.ex (4069B)
1 defmodule Ecto.Repo.Assoc do 2 # The module invoked by repo modules 3 # for association related functionality. 4 @moduledoc false 5 6 @doc """ 7 Transforms a result set based on query assocs, loading 8 the associations onto their parent schema. 9 """ 10 @spec query([list], list, tuple, (list -> list)) :: [Ecto.Schema.t] 11 def query(rows, assocs, sources, fun) 12 13 def query([], _assocs, _sources, _fun), do: [] 14 def query(rows, [], _sources, fun), do: Enum.map(rows, fun) 15 16 def query(rows, assocs, sources, fun) do 17 # Create rose tree of accumulator dicts in the same 18 # structure as the fields tree 19 accs = create_accs(0, assocs, sources, []) 20 21 # Populate tree of dicts of associated entities from the result set 22 {_keys, _cache, rows, sub_dicts} = Enum.reduce(rows, accs, fn row, acc -> 23 merge(fun.(row), acc, 0) |> elem(0) 24 end) 25 26 # Create the reflections that will be loaded into memory. 27 refls = create_refls(0, assocs, sub_dicts, sources) 28 29 # Retrieve and load the assocs from cached dictionaries recursively 30 for {item, sub_structs} <- Enum.reverse(rows) do 31 [load_assocs(item, refls)|sub_structs] 32 end 33 end 34 35 defp merge([struct|sub_structs], {primary_keys, cache, dict, sub_dicts}, parent_key) do 36 child_key = 37 if struct do 38 for primary_key <- primary_keys do 39 case Map.get(struct, primary_key) do 40 nil -> raise Ecto.NoPrimaryKeyValueError, struct: struct 41 value -> value 42 end 43 end 44 end 45 46 # Traverse sub_structs adding one by one to the tree. 47 # Note we need to traverse even if we don't have a child_key 48 # due to nested associations. 49 {sub_dicts, sub_structs} = Enum.map_reduce(sub_dicts, sub_structs, &merge(&2, &1, child_key)) 50 51 cache_key = cache_key(parent_key, child_key, sub_structs, dict) 52 53 if struct && parent_key && not Map.get(cache, cache_key, false) do 54 cache = Map.put(cache, cache_key, true) 55 item = {child_key, struct} 56 57 # If we have a list, we are at the root, so we also store the sub structs 58 dict = update_dict(dict, parent_key, item, sub_structs) 59 60 {{primary_keys, cache, dict, sub_dicts}, sub_structs} 61 else 62 {{primary_keys, cache, dict, sub_dicts}, sub_structs} 63 end 64 end 65 66 defp cache_key(parent_key, child_key, sub_structs, dict) when is_list(dict) do 67 {parent_key, child_key, sub_structs} 68 end 69 70 defp cache_key(parent_key, child_key, _sub_structs, dict) when is_map(dict) do 71 {parent_key, child_key} 72 end 73 74 defp update_dict(dict, _parent_key, item, sub_structs) when is_list(dict) do 75 [{item, sub_structs} | dict] 76 end 77 78 defp update_dict(dict, parent_key, item, _sub_structs) when is_map(dict) do 79 Map.update(dict, parent_key, [item], &[item | &1]) 80 end 81 82 defp load_assocs({child_key, struct}, refls) do 83 Enum.reduce refls, struct, fn {dict, refl, sub_refls}, acc -> 84 %{field: field, cardinality: cardinality} = refl 85 loaded = 86 dict 87 |> Map.get(child_key, []) 88 |> Enum.reverse() 89 |> Enum.map(&load_assocs(&1, sub_refls)) 90 |> maybe_first(cardinality) 91 Map.put(acc, field, loaded) 92 end 93 end 94 95 defp maybe_first(list, :one), do: List.first(list) 96 defp maybe_first(list, _), do: list 97 98 defp create_refls(idx, fields, dicts, sources) do 99 {_source, schema, _prefix} = elem(sources, idx) 100 101 Enum.map(:lists.zip(dicts, fields), fn 102 {{_primary_keys, _cache, dict, sub_dicts}, {field, {child_idx, child_fields}}} -> 103 sub_refls = create_refls(child_idx, child_fields, sub_dicts, sources) 104 {dict, schema.__schema__(:association, field), sub_refls} 105 end) 106 end 107 108 defp create_accs(idx, fields, sources, initial_dict) do 109 acc = Enum.map(fields, fn {_field, {child_idx, child_fields}} -> 110 create_accs(child_idx, child_fields, sources, %{}) 111 end) 112 113 {_source, schema, _prefix} = elem(sources, idx) 114 115 case schema.__schema__(:primary_key) do 116 [] -> raise Ecto.NoPrimaryKeyFieldError, schema: schema 117 pk -> {pk, %{}, initial_dict, acc} 118 end 119 end 120 end