jarmo
How to handle pattern matching inside enumerators
I have a situation in my Phoenix web application where I’m performing multiple INSERT statements with ecto and I would like to roll back my transaction when any of the insert statements fail and have access to the first possible Ecto.Changeset validation error.
I have something like this:
in_transaction(fn ->
items
|> Enum.each(fn item ->
create_item(item)
end)
end)
Where create_item/1 is nothing more than an ecto call to create an item via Ecto.Changeset so it will return {:ok, item} on a successful case and an error tuple for unsuccessful case and in_transaction/1 is just some boiler-plate to hide Ecto.Multi logic.
My first attempt to solve my problem was to use with statement inside Enum.each/2:
in_transaction(fn ->
items
|> Enum.each(fn item ->
with {:ok, _item} <- create_item(item) do
_item
end
end)
end)
However, this does not seem to have any effect. I also tried this:
in_transaction(fn ->
items
|> Enum.each(fn item ->
{:ok, _item} = create_item(item)
end)
end)
Which does throw MatchError, but is not something I would like to handle inside my in_transaction/1 function via try/rescue to get the possible %Ecto.Changeset{errors: errors}.
Is there a better way to short-circuit enumeration when something inside of the enumeration does not match or is try/rescue really the only way? It just doesn’t feel Elixir-way.
Marked As Solved
hlx
I meant something in the lines of
Enum.reduce_while(items, :ok, fn item, acc ->
case create_item(item) do
{:ok, _item} -> {:cont, acc}
{:error, changeset} -> {:halt, {:error, changeset})
end
end)
end
Also Liked
dimitarvp
Absolutely not.
For that you use Ecto.Multi which is amazingly well documented.
We here are kind of trying to generalize a non-Ecto solution because OP said they used Ecto only as an example and needed to get familiar with some Elixir patterns (in terms of collections e.g. lists).
Though I believe that he can now see how an example that’s not picked well can confuse future readers. ![]()
jarmo
It seems that all of this got quite complex really fast due to my obscure example.
I’m trying to make it as specific as possible.
in_transaction/1 is defined in MyApp.Repo:
defmodule MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
alias Ecto.Multi
alias MyApp.Repo
def in_transaction(fun) do
with {:ok, %{model: result}} <- run_in_transaction(fun) do
{:ok, result}
else
{:error, :model, error, _} ->
{:error, error}
end
end
defp run_in_transaction(fun) do
Multi.new
|> Multi.run(
:model,
fn _repo, _ ->
fun.()
end
)
|> Repo.transaction
end
end
create_item/1 is pretty standard to insert an Ecto model into the database:
def create_item(attrs) do
changeset(%Item{}, attrs)
|> MyApp.Repo.insert # returns either {:ok, %Item{} = item} or {:error, %Ecto.Changeset{valid?: false, errors: [...]}} under normal circumstances
end
This abstraction hides Ecto.Multi logic into one module and allows to choose to use a transaction between multiple inserts or not by not changing any Ecto.Schema models.
These building blocks allow me to write code like this:
MyApp.Repo.in_transaction(fn ->
create_item(%{foo: "bar"})
end)
Code above will either commit %Item{} to the database or roll changes back if there are some problems and will return these problems to the in_transaction caller. Of course inserting one entity does not need a separate transaction and code like this makes more sense:
MyApp.Repo.in_transaction(fn ->
with {:ok, _item1} <- create_item(%{foo: "bar"}),
{:ok, _item2} <- create_item(%{foo: "baz"}) do
{:ok, :all_good} # just so that Ecto.Multi would not roll back
end
end)
Now, if _item2 creation fails then _item1 will be rolled back as well and this is what we want by using database transactions. So far so good.
Or if I don’t care about transactions/rolling back then code like this works too:
with {:ok, _item1} <- create_item(%{foo: "bar"}),
{:ok, _item2} <- create_item(%{foo: "baz"}) do
{:ok, :all_good}
end
However, if I have an arbitrary amount of items then the original question became apparent.
For example, this doesn’t work:
def create_all_items(items) do
Enum.each(items, fn item ->
create_item(item)
end) # always traverses whole list and even if some item failed to be created then this information will not be passed up to the caller of `in_transaction/1`
{:ok, :all_good} # just so that Ecto.Multi would not roll back, but I'm not sure anymore if anything gets committed in case of any errors inside any `create_item/1`.
end
I tried something like this:
def create_all_items(items) do
Enum.each(items, fn item ->
with {:ok, item} <- create_item(item) do
{:ok, item}
end
end) # still returns always :ok and traverses whole list even on non-matches?
end
Then I got to this, which kind of works, but looks really verbose and unexpected by Enum.find/2 to have any side-effect via calling create_item/1:
def create_all_items(items) do
failed_created_item = Enum.find(items, fn item ->
!match?({:ok, _item}, create_item(item))
end)
if failed_created_item do
failed_created_item # most of the time returns {:error, %Ecto.Changeset{valid?: false, errors: [...]}}
else
{:ok, items} # just so that Ecto.Multi would not roll back
end
end
Basically this stops calling create_item/1 as soon as some item failed to be created, e.g. returned something else than {:ok, item} and the validation error will be passed up to the Ecto.Multi and to the caller of in_transaction/1. I guess the suggested Enum.reduce_while would be as weird and not less verbose.
I ended up doing like this:
def create_all_items(items) do
Enum.each(items, fn item ->
{:ok, item} = create_item(item) # this will raise MatchError on errors, but MyApp.Repo.in_transaction needs modifying
end)
{:ok, :all_good} # just so that Ecto.Multi would not roll back
end
However this requires me to modify run_in_transaction/1 to the following:
defp run_in_transaction(fun) do
try do
Multi.new
|> Multi.run(
:model,
fn _repo, _ ->
fun.()
end
)
|> Repo.transaction
rescue
error in MatchError ->
with %MatchError{term: {:error, %Ecto.Changeset{}} = validation_error} <- error do
validation_error
else
_ ->
reraise error, __STACKTRACE__
end
end
end
It looks really ugly due to the fact that I need to have extra with statement inside rescue clause to match only Ecto.Changeset errors and to use reraise to raise any other errors. It’s also very error-prone since it will not work as expected when I’m going to write with statement using <- instead of a regular match operator =. It does not feel something that a real Elixir dev would write. Prove me wrong.
PS! This got me to the next more abstract question about quitting Enum iterator early on from any function (each, map, reduce - you call it). For example in Ruby you can do something like this:
items.each do |item|
break if item.foo
end
Is there some equivalent pattern/way in Elixir?
hlx
Though I really like @sasajuric 's abstraction with Repo.transact/1 from Towards Maintainable Elixir: The Core and the Interface | by Saša Jurić | Very Big Things | Medium
sasajuric
I disagree. I think Enum.reduce_while wouldn’t be as weird. But this looks like an example where recursion would be particularly elegant:
def create_items([]), do: {:ok, []}
def create_items([item | items]) do
with {:ok, stored_item} <- create_item(item),
{:ok, stored_items} <- create_items(items),
do: {:ok, [stored_item | stored_items]}
end
Alternatively, as has been mentioned, you can repeatedly add records to multi using Multi.insert. Once you submit such multi to a transaction, it will either succeed completely or fail on first error (and return that error as a result).
jarmo
I went with this solution in the end and removed try/rescue from in_transaction/1. It looks pretty okay in the end. Thank you for suggesting this.








