A query is the input of one read use case, and it carries its own handler.
Like a DDD.Command it declares fields (validated on construction) and implements
execute/1, but it only reads: it never saves an aggregate or emits an event.
defmodule MyApp.Queries.OpenItems do
use DDD.Query
query do
description "The items of a list that are not done yet"
field :list_id, :uuid, required: true, description: "The list to read"
end
@impl DDD.Query
def execute(%__MODULE__{list_id: id}) do
with {:ok, list} <- MyApp.ListRepository.impl().find_by_id(id) do
{:ok, for(item <- list.items, item.status == :open, do: %{id: item.id})}
end
end
end
MyApp.Queries.OpenItems.call(%{list_id: id}) # => {:ok, [%{id: ...}]}new/1, call/1 and call!/1 work as for commands, and an error comes back as a
DDD.Error whose context has :query set to the query module.
Read-only transaction
execute/1 runs in a read-only database transaction by default. Postgres refuses any
write inside it, so "this cannot change data" is enforced by the database. The transaction
also gives the query one consistent snapshot, and it is always rolled back, never committed.
use DDD.Query # read-only transaction on the configured repo
use DDD.Query, repo: MyApp.Repo # another repo
use DDD.Query, transaction: false # no transaction (in-memory repositories, APIs)A query called from inside an open transaction (for example from a command) joins it and cannot make it read-only.