I usually avoid making request to a database inside changesets. Instead, I use more explicit multis or transactions. So your second example I would write as:
def changeset(record, attrs) do
cast(record, attrs, [:issued_at, :due_for_return, :returned_at, :fee, :fee_paid_at])
end
defp valid_book_multi(multi, book_id) do
alias Ecto.Multi
Multi.run(multi, :book, fn _changes ->
case Book.get(book_id) do
%Book{status: :available} = available_book -> {:ok, available_book}
%Book{status: status} = not_available_book -> {:error, Book.error_msg(:status, status)}
nil -> {:error, :not_found}
end
end)
end
@spec create_record(%{(String.t | atom) => term}, book_id: pos_integer) :: {:ok, %Record{}} | {:error, Ecto.Changeset.t}
def create_record(attrs, book_id: book_id) do
alias Ecto.Multi
record_changeset = changeset(%Record{book_id: book_id}, attrs)
Multi.new()
|> valid_book_multi(book_id)
|> Multi.insert(:record, record_changeset)
|> Repo.transaction()
|> case do
{:ok, %{record: record}} -> {:ok, record}
{:error, :book, reason, _changes} -> # collect errors into record changesets
end
end