Creating users within a transaction

Actually, I think I found your problem. Multi has changed behavior since 2.1.6.

here are the relevant docs for you: Ecto.Multi – Ecto v2.1.6

Do you see the problem? it says “The function…receives changes so far as an argument.”

So you should pass only changes_so_far and not pass repo.

like this:

      Multi.run(multi_acc, String.to_atom("user#{index}"), fn _changes_so_far -> 
        {:ok, add_user(user_attrs)}
      end)

and also here:

    |> Multi.run(:ensure_all_ok, fn changes_so_far ->

@ConstantBall Does that fix it for you?

Ecto.Multi in Ecto 3 has had a breaking change if you come from version 2.

I cannot believe I missed that. Thanks for catching it. I’m no longer getting an error on Multi.run, although I’m now getting a WithClauseError after the |> Repo.transaction() stating that no clause matching: %User{...} which I have not been able to make sense of. I don’t see why it would calling add_user/1 given that I haven’t changed the code.

I can help you better if you paste the full error verbatim.

Can you IO.inspect what your with expression is returning in add_user/1?

You can do something like this:

with {roles, user_attrs} = Map.pop(user_attrs, :roles),
         {:ok, user} <-
           %User{}
           |> User.changeset(user_attrs)
           |> Repo.insert(),
         _ <- add_roles(user, roles),
         _ <- add_extension(user, user_attrs) do
      {:ok, user} |> IO.inspect(label: "OK response")
    else
      {:error, changeset} = error ->
        error
        |> IO.inspect(label: "changeset error")

      anything_else ->
        IO.inspect(anything_else, label: "anything else")
    end
1 Like

Adding IO.inspect in the with expression as you suggest and using the following input data where it’s expected to fail because of the second and fourth users (due to duplicate emails and usernames, respectively):

"""
"One Test", onetest@example.com, one.test
"Test, Two", onetest@example.com, two
"Three Test", threetest@example.com, threetest
"Test, Four", fourtest@example.com, threetest
"""

The output is the following:

Anything else: %Accounts.User{
  email: "onetest@example.com",
  password: "one.test99",
  name: "One Test",
  username: "one.test",
  ...
}
Changeset error: {:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{
     account: #Ecto.Changeset<action: :insert, changes: %{}, errors: [],
      data: #Accounts.Account<>, valid?: true>,
     email: "onetest@example.com",
     name: "Two Test",
     password: "two99",
     username: "two",
   },
   errors: [email: {"has already been taken", []}],
   data: #Accounts.User<>,
   valid?: false
 >}
** (DBConnection.ConnectionError) transaction rolling back
    (db_connection) lib/db_connection.ex:1531: DBConnection.fetch_info/1
    (db_connection) lib/db_connection.ex:1232: DBConnection.transaction_meter/3
    (db_connection) lib/db_connection.ex:798: DBConnection.transaction/3
    iex:164: ImportUsers.add_user/1
    iex:142: anonymous fn/2 in ImportUsers.add_users/1
    (ecto) lib/ecto/multi.ex:422: Ecto.Multi.apply_operation/5
    (elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ecto) lib/ecto/multi.ex:412: anonymous fn/5 in Ecto.Multi.apply_operations/5

The “anything else” is your problem. It’s a return value not expected by your with expression. I don’t know what part of your code is returning the %User{} struct but that’s why this problem is happening.

To put it another way, add_user/1 isn’t working in the way you expect. It needs to always return {:ok, something} or {:error, something}. This isn’t happening because one of the function in your with clause is returning %User{} struct.

You can debug it yourself from here. IO.inspect everything and find out why your with clause is returning a bare %User{} struct.

P.S. this is your "with clause":

{roles, user_attrs} = Map.pop(user_attrs, :roles),
         {:ok, user} <-
           %User{}
           |> User.changeset(user_attrs)
           |> Repo.insert(),
         _ <- add_roles(user, roles),
         _ <- add_extension(user, user_attrs)

so try something like this:

{roles, user_attrs} = Map.pop(user_attrs, :roles) |> IO.inspect(label: "with clause function 0"),
         {:ok, user} <-
           %User{}
           |> User.changeset(user_attrs)
           |> Repo.insert()  |> IO.inspect(label: "with clause function 1"),
         _ <- add_roles(user, roles)  |> IO.inspect(label: "with clause function 2"),
         _ <- add_extension(user, user_attrs) |> IO.inspect(label: "with clause function 3")
2 Likes

All of your help has been greatly appreciated. Thank you.

1 Like