Flop does not add joins to your query. A join field points at a named binding, and that binding has to be present in the query you pass to Flop. The straightforward way is to add every join up front, but then, the database will join tables that most requests neither filter nor order by.
With Flop.with_named_bindings/4, you pass a function that knows how to add
each binding, and Flop applies it only for the bindings the given parameters
need.
Query
In this example, we are going to add a join_assocs/2 function to the schema
module. Each binding is covered by its own function clause.
defmodule MyApp.Pet do
use Ecto.Schema
import Ecto.Query
# ...
def join_assocs(query, :owner) do
join(query, :left, [pet: p], o in assoc(p, :owner), as: :owner)
end
def join_assocs(query, :breed) do
join(query, :left, [pet: p], b in assoc(p, :breed), as: :breed)
end
endThe function takes the query and the name of a binding and returns the query
with that binding added. It has the same arguments and the same return value as
the one you pass to Ecto.Query.with_named_binding/3.
We are using a left join here, because an inner join would drop every pet without an owner or breed.
Flop.with_named_bindings/4 needs a validated Flop struct, so validate the
parameters first and run the query in a second step.
def list_pets(params) do
opts = [for: Pet]
with {:ok, flop} <- Flop.validate(params, opts) do
Pet
|> from(as: :pet)
|> Flop.with_named_bindings(flop, &Pet.join_assocs/2, opts)
|> Flop.run(flop, opts)
end
endAliasing the source with as: :pet lets the callback refer to it by name.
Required bindings
Flop.with_named_bindings/4 determines the bindings with
Flop.named_bindings/3, which you can also call on its own. It returns:
- the binding of every join field in the filters or in the order clause
- the bindings of the join fields inside a compound field
- the bindings you configured with the
bindingsoption of a custom field
Filters with a nil value are left out, and Flop casts a blank string to nil,
so an empty input in a filter form does not result in a binding.
The callback has to handle every binding in that list. If a join field points at the source binding, add a clause that returns the query unchanged.
def join_assocs(query, :pet), do: queryJoins you always need
Some joins are needed regardless of the parameters, because the select clause or a preload uses them. Add those first and let Flop add the rest.
def list_pets(params) do
opts = [for: Pet]
with {:ok, flop} <- Flop.validate(params, opts) do
Pet
|> from(as: :pet)
|> join_required_assocs([:owner])
|> Flop.with_named_bindings(flop, &Pet.join_assocs/2, opts)
|> preload([owner: o], owner: o)
|> Flop.run(flop, opts)
end
end
defp join_required_assocs(query, bindings) do
Enum.reduce(bindings, query, fn binding, query ->
Ecto.Query.with_named_binding(query, binding, &Pet.join_assocs/2)
end)
endEcto.Query.with_named_binding/3 returns the query unchanged if the binding is
already there, and Flop.with_named_bindings/4 goes through it, so a binding
you added yourself is not added a second time.
Intermediate joins
A nested association needs the join in between. The callback can add it by
calling itself through Ecto.Query.with_named_binding/3.
def join_assocs(query, :owner) do
join(query, :left, [pet: p], o in assoc(p, :owner), as: :owner)
end
def join_assocs(query, :address) do
query
|> Ecto.Query.with_named_binding(:owner, &join_assocs/2)
|> join(:left, [owner: o], a in assoc(o, :address), as: :address)
endThe same check applies here, so the intermediate join is added once, no matter how many of the requested fields sit behind it.
Joins that also select
A callback can do more than join. The lateral join for a computed field also selects the value, which is then only in the result when the binding is needed.
def join_assocs(query, :computed) do
from p in query,
inner_lateral_join: c in fragment("SELECT lower(?) AS name_lower", p.name),
on: true,
as: :computed,
select_merge: %{name_lower: c.name_lower}
endHere name_lower is nil on the returned structs when neither the filters nor
the order refer to it. If you use the value outside of Flop, for example to
display it, add the join unconditionally instead.
Counting
With page or offset pagination, Flop counts with the query you pass it, so the count query has the same conditional joins. It counts without the order clause, so the joins that only the order needs do nothing for the count.
order: false gives you the bindings for the filters only. Build a second query
with it and pass it as count_query.
def list_pets(params) do
opts = [for: Pet]
with {:ok, flop} <- Flop.validate(params, opts) do
base = from(Pet, as: :pet)
query = Flop.with_named_bindings(base, flop, &Pet.join_assocs/2, opts)
count_query =
Flop.with_named_bindings(
base,
flop,
&Pet.join_assocs/2,
Keyword.put(opts, :order, false)
)
Flop.run(query, flop, Keyword.put(opts, :count_query, count_query))
end
endComplete example
defmodule MyApp.Pet do
use Ecto.Schema
import Ecto.Query
@derive {Flop.Schema,
filterable: [:name, :owner_name, :breed_name],
sortable: [:name, :owner_name, :breed_name],
default_order: %{
order_by: [:name],
order_directions: [:asc]
},
adapter_opts: [
join_fields: [
owner_name: [
binding: :owner,
field: :name,
ecto_type: :string
],
breed_name: [
binding: :breed,
field: :name,
ecto_type: :string
]
]
]}
schema "pets" do
field :name, :string
belongs_to :owner, MyApp.Owner
belongs_to :breed, MyApp.Breed
end
def join_assocs(query, :owner) do
join(query, :left, [pet: p], o in assoc(p, :owner), as: :owner)
end
def join_assocs(query, :breed) do
join(query, :left, [pet: p], b in assoc(p, :breed), as: :breed)
end
end
defmodule MyApp.Pets do
import Ecto.Query
alias MyApp.Pet
def list_pets(params) do
opts = [for: Pet]
with {:ok, flop} <- Flop.validate(params, opts) do
Pet
|> from(as: :pet)
|> Flop.with_named_bindings(flop, &Pet.join_assocs/2, opts)
|> Flop.run(flop, opts)
end
end
end