Current section

Files

Jump to
ecto lib ecto repo preloader.ex
Raw

lib/ecto/repo/preloader.ex

defmodule Ecto.Repo.Preloader do
# The module invoked by user defined repos
# for preload related functionality.
@moduledoc false
require Ecto.Query
@doc """
Transforms a result set based on query preloads, loading
the associations onto their parent schema.
"""
@spec query([list], Ecto.Repo.t, list, Access.t, fun, Keyword.t) :: [list]
def query([], _repo, _preloads, _take, _fun, _opts), do: []
def query(rows, _repo, [], _take, fun, _opts), do: Enum.map(rows, fun)
def query(rows, repo, preloads, take, fun, opts) do
rows
|> extract
|> do_preload(repo, preloads, take, opts)
|> unextract(rows, fun)
end
defp extract([[nil|_]|t2]), do: extract(t2)
defp extract([[h|_]|t2]), do: [h|extract(t2)]
defp extract([]), do: []
defp unextract(structs, [[nil|_] = h2|t2], fun), do: [fun.(h2)|unextract(structs, t2, fun)]
defp unextract([h1|structs], [[_|t1]|t2], fun), do: [fun.([h1|t1])|unextract(structs, t2, fun)]
defp unextract([], [], _fun), do: []
@doc """
Implementation for `Ecto.Repo.preload/2`.
"""
@spec preload(structs, atom, atom | list, Keyword.t) ::
structs when structs: [Ecto.Schema.t] | Ecto.Schema.t
def preload(structs, repo, preloads, opts) when is_list(structs) do
do_preload(structs, repo, preloads, opts[:take], opts)
end
def preload(struct, repo, preloads, opts) when is_map(struct) do
do_preload([struct], repo, preloads, opts[:take], opts) |> hd()
end
defp do_preload(structs, repo, preloads, take, opts) do
preloads = normalize(preloads, take, preloads)
preload_each(structs, repo, preloads, opts)
rescue
e ->
# Reraise errors so we ignore the preload inner stacktrace
reraise e
end
## Preloading
defp preload_each(structs, _repo, [], _opts), do: structs
defp preload_each([], _repo, _preloads, _opts), do: []
defp preload_each([sample|_] = structs, repo, preloads, opts) do
module = sample.__struct__
prefix = preload_prefix(opts, sample)
{assocs, throughs} = expand(module, preloads, {%{}, %{}})
assocs =
maybe_pmap Map.values(assocs), repo, opts, fn
{{:assoc, assoc, related_key}, take, query, sub_preloads}, opts ->
preload_assoc(structs, module, repo, prefix, assoc, related_key,
query, sub_preloads, take, opts)
end
throughs =
Map.values(throughs)
for struct <- structs do
struct = Enum.reduce assocs, struct, &load_assoc/2
struct = Enum.reduce throughs, struct, &load_through/2
struct
end
end
defp preload_prefix(opts, sample) do
case Keyword.fetch(opts, :prefix) do
{:ok, prefix} ->
prefix
:error ->
%{__meta__: %{source: {prefix, _}}} = sample
prefix
end
end
## Association preloading
defp maybe_pmap(assocs, repo, opts, fun) do
if match?([_,_|_], assocs) and not repo.in_transaction? and
Keyword.get(opts, :in_parallel, true) do
# We pass caller: self() so pools like the ownership
# pool knows where to fetch the connection from and
# set the proper timeouts.
opts = Keyword.put_new(opts, :caller, self())
assocs
|> Enum.map(&Task.async(:erlang, :apply, [fun, [&1, opts]]))
|> Enum.map(&Task.await(&1, :infinity))
else
Enum.map(assocs, &fun.(&1, opts))
end
end
defp preload_assoc(structs, module, repo, prefix, %{cardinality: card} = assoc,
related_key, query, preloads, take, opts) do
{fetch_ids, loaded_ids, loaded_structs} =
fetch_ids(structs, module, assoc, opts)
{fetch_ids, fetch_structs} =
fetch_query(fetch_ids, assoc, repo, query, prefix, related_key, take, opts)
all = preload_each(Enum.reverse(loaded_structs, fetch_structs), repo, preloads, opts)
{:assoc, assoc, assoc_map(card, Enum.reverse(loaded_ids, fetch_ids), all)}
end
defp fetch_ids(structs, module, assoc, opts) do
%{field: field, owner_key: owner_key, cardinality: card} = assoc
force? = Keyword.get(opts, :force, false)
Enum.reduce structs, {[], [], []}, fn struct, {fetch_ids, loaded_ids, loaded_structs} ->
assert_struct!(module, struct)
%{^owner_key => id, ^field => value} = struct
cond do
card == :one and not is_nil(value) and Ecto.assoc_loaded?(value) and not force? ->
{fetch_ids, [id|loaded_ids], [value|loaded_structs]}
card == :many and Ecto.assoc_loaded?(value) and not force? ->
{fetch_ids,
List.duplicate(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
defp fetch_query([], _assoc, _repo, _query, _prefix, _related_key, _take, _opts) do
{[], []}
end
defp fetch_query(ids, _assoc, _repo, query, _prefix, {_, key}, _take, _opts) when is_function(query, 1) do
data = ids |> Enum.uniq |> query.() |> Enum.map(&{Map.fetch!(&1, key), &1}) |> Enum.sort
unzip_ids data, [], []
end
defp fetch_query(ids, %{cardinality: card} = assoc, repo, query, prefix, related_key, take, opts) do
query = assoc.__struct__.assoc_query(assoc, query, Enum.uniq(ids))
field = related_key_to_field(query, related_key)
# Normalize query
query = %{Ecto.Query.Planner.returning(query, take || true) | prefix: prefix}
# Add the related key to the query results
query = update_in query.select.expr, &{:{}, [], [field, &1]}
# If we are returning many results, we must sort by the key too
query =
case card do
:many ->
update_in query.order_bys, fn order_bys ->
[%Ecto.Query.QueryExpr{expr: [asc: field], params: [],
file: __ENV__.file, line: __ENV__.line}|order_bys]
end
:one ->
query
end
unzip_ids repo.all(query, opts), [], []
end
defp related_key_to_field(query, {pos, key}) do
{{:., [], [{:&, [], [related_key_pos(query, pos)]}, key]}, [], []}
end
defp related_key_pos(_query, pos) when pos >= 0, do: pos
defp related_key_pos(query, pos), do: Ecto.Query.Builder.count_binds(query) + pos
defp unzip_ids([{k, v}|t], acc1, acc2), do: unzip_ids(t, [k|acc1], [v|acc2])
defp unzip_ids([], acc1, acc2), do: {acc1, acc2}
defp assert_struct!(mod, %{__struct__: mod}), do: true
defp assert_struct!(mod, %{__struct__: struct}) do
raise ArgumentError, "expected a homogeneous list containing the same struct, " <>
"got: #{inspect mod} and #{inspect struct}"
end
defp assoc_map(:one, ids, structs) do
one_assoc_map(ids, structs, %{})
end
defp assoc_map(:many, ids, structs) do
many_assoc_map(ids, structs, %{})
end
defp one_assoc_map([id|ids], [struct|structs], map) do
one_assoc_map(ids, structs, Map.put(map, id, struct))
end
defp one_assoc_map([], [], map) do
map
end
defp many_assoc_map([id|ids], [struct|structs], map) do
{ids, structs, acc} = split_while(ids, structs, id, [struct])
many_assoc_map(ids, structs, Map.put(map, id, acc))
end
defp many_assoc_map([], [], map) do
map
end
defp split_while([id|ids], [struct|structs], id, acc),
do: split_while(ids, structs, id, [struct|acc])
defp split_while(ids, structs, _id, acc),
do: {ids, structs, acc}
## Load preloaded data
defp load_assoc({:assoc, assoc, ids}, struct) do
%{field: field, owner_key: owner_key, cardinality: cardinality} = assoc
key = Map.fetch!(struct, owner_key)
loaded =
case ids do
%{^key => value} -> value
_ when cardinality == :many -> []
_ -> nil
end
Map.put(struct, field, loaded)
end
defp load_through({:through, assoc, throughs}, struct) do
%{cardinality: cardinality, field: field, owner: owner} = assoc
{loaded, _} = Enum.reduce(throughs, {[struct], owner}, &recur_through/2)
Map.put(struct, field, maybe_first(loaded, cardinality))
end
defp maybe_first(list, :one), do: List.first(list)
defp maybe_first(list, _), do: list
defp recur_through(field, {structs, owner}) do
assoc = owner.__schema__(:association, field)
case assoc.__struct__.preload_info(assoc) do
{:assoc, %{related: related}, _} ->
pks = related.__schema__(:primary_key)
{children, _} =
Enum.reduce(structs, {[], %{}}, fn struct, acc ->
children = struct |> Map.fetch!(field) |> List.wrap
Enum.reduce children, acc, fn child, {fresh, set} ->
keys = through_pks(child, pks, assoc)
case set do
%{^keys => true} ->
{fresh, set}
_ ->
{[child|fresh], Map.put(set, keys, true)}
end
end
end)
{Enum.reverse(children), related}
{:through, _, through} ->
Enum.reduce(through, {structs, owner}, &recur_through/2)
end
end
defp through_pks(map, pks, assoc) do
Enum.map pks, fn pk ->
case map do
%{^pk => value} -> value
_ ->
raise ArgumentError,
"cannot preload through association `#{assoc.field}` on `#{inspect assoc.owner}`. " <>
"Ecto expected a map/struct with the key `#{pk}` but got: #{inspect map}"
end
end
end
## Normalizer
def normalize(preload, take, original) do
normalize_each(wrap(preload, original), [], take, original)
end
defp normalize_each({atom, {query, list}}, acc, take, original)
when is_atom(atom) and (is_map(query) or is_function(query, 1)) do
fields = take(take, atom)
[{atom, {fields, query!(query), normalize_each(wrap(list, original), [], fields, original)}}|acc]
end
defp normalize_each({atom, query}, acc, take, _original)
when is_atom(atom) and (is_map(query) or is_function(query, 1)) do
[{atom, {take(take, atom), query!(query), []}}|acc]
end
defp normalize_each({atom, list}, acc, take, original) when is_atom(atom) do
fields = take(take, atom)
[{atom, {fields, nil, normalize_each(wrap(list, original), [], fields, original)}}|acc]
end
defp normalize_each(atom, acc, take, _original) when is_atom(atom) do
[{atom, {take(take, atom), nil, []}}|acc]
end
defp normalize_each(other, acc, take, original) do
Enum.reduce(wrap(other, original), acc, &normalize_each(&1, &2, take, original))
end
defp query!(query) when is_function(query, 1), do: query
defp query!(%Ecto.Query{} = query), do: query
defp take(take, field) do
case Access.fetch(take, field) do
{:ok, fields} -> List.wrap(fields)
:error -> nil
end
end
defp wrap(list, _original) when is_list(list),
do: list
defp wrap(atom, _original) when is_atom(atom),
do: atom
defp wrap(other, original) do
raise ArgumentError, "invalid preload `#{inspect other}` in `#{inspect original}`. " <>
"preload expects an atom, a (nested) keyword or a (nested) list of atoms"
end
## Expand
def expand(schema, preloads, acc) do
Enum.reduce(preloads, acc, fn {preload, {fields, query, sub_preloads}}, {assocs, throughs} ->
assoc = Ecto.Association.association_from_schema!(schema, preload)
info = assoc.__struct__.preload_info(assoc)
case info do
{:assoc, _, _} ->
value = {info, fields, query, sub_preloads}
assocs = Map.update(assocs, preload, value, &merge_preloads(preload, value, &1))
{assocs, throughs}
{:through, _, through} ->
through =
through
|> Enum.reverse()
|> Enum.reduce({fields, query, sub_preloads}, &{nil, nil, [{&1, &2}]})
|> elem(2)
expand(schema, through, {assocs, Map.put(throughs, preload, info)})
end
end)
end
defp merge_preloads(_preload, {info, _, nil, left}, {info, take, query, right}),
do: {info, take, query, left ++ right}
defp merge_preloads(_preload, {info, take, query, left}, {info, _, nil, right}),
do: {info, take, query, left ++ right}
defp merge_preloads(preload, {info, _, left, _}, {info, _, right, _}) do
raise ArgumentError, "cannot preload `#{preload}` as it has been supplied more than once " <>
"with different queries: #{inspect left} and #{inspect right}"
end
defp reraise(exception) do
reraise exception, Enum.reject(System.stacktrace, &match?({__MODULE__, _, _, _}, &1))
end
end