From `Repo.insert/2` to `Repo.transaction/2`

I have to convert the following code using Repo.insert/2

def create_question(attrs) do
  %Question{}
  |> Question.creation_changeset(attrs)
  |> Repo.insert()
end

to use a transaction:

def create_question(attrs) do
  question_changeset =
    Question.creation_changeset(%Question{}, attrs)

  Multi.new()
  |> Multi.insert(:question, question_changeset)
  |> Multi.run(:do_something, fn _repo, %{question: question} ->
    # do something
    {:ok, nil}
  end)
  |> Repo.transaction()
end

The problem is that now I have to change the code of my controller because the return value of Repo.transaction/2 is different than the return value of Repo.insert/2.

The controller’s action code is the following:

def create(conn, %{"question" => question_params}) do
  case Quiz.create_question(question_params) do
    {:ok, question} ->
      conn
      |> put_flash(:info, "Question created successfully.")
      |> redirect(to: Routes.admin_question_path(conn, :show, question))

    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

I think it’s wrong to change controller’s code whether I use a transaction or not? I think that the controller always either needs a struct or a changeset to display the errors, just as Repo.insert/2 returns?

Any advice is welcome on how to refactor that properly, on where I have to change code.

:wave:

If you don’t need the full result including :do_something, then you can return the same tagged tuple as before:

def create_question(attrs) do
  question_changeset =
    Question.creation_changeset(%Question{}, attrs)

  Multi.new()
  |> Multi.insert(:question, question_changeset)
  |> Multi.run(:do_something, fn _repo, %{question: question} ->
    # do something
    {:ok, nil}
  end)
  |> Repo.transaction()
  |> case do
    {:ok, %{question: question}} -> {:ok, question}
    {:error, :question, %Ecto.Changeset{} = changeset, _changes} -> {:error, changeset}
  end
end
2 Likes

Thank you.

What if this :do_something is actually storing file uploads, and I want to let the user know if the file upload failed instead of crashing the app?

So somehow return the changeset of question in case of errors as you did above, but in addition to that also add an error into that changeset if the storing of file uploads failed.

You want to change the behavior of the function, but not change the values returned? You could manually add an error (and action) to question_changeset if you really want to. Just be aware that you might encounter errors, which are not really related to the form itself and therefore don’t fit that option.

2 Likes

For me the biggest pain in Elixir currently is knowing what to do in case of errors.

What do you think about this? Your opinion is really helpful.

Multi.new()
|> Multi.insert(:question, question_changeset)
|> Multi.run(:upload_files, #some code to upload the files
|> Repo.transaction()
|> case do
  {:ok, %{question: question}} ->
    {:ok, question}

  {:error, :question, %Ecto.Changeset{} = changeset, _changes} ->
    {:error, changeset}

  {:error, :upload_files, {:file_path_exists, file_path}, _changes} ->
    raise "file upload failed: path \"#{file_path}\" already exists"
end

As you can see I let it crash in case of failed file upload, but I still want to emit a custom error message (instead of getting some obscure error message such as clause not matching somewhere…).

If you think that’s good, I still need to see how to handle that in production, i.e. display a nice error page for the user, and notify myself of the error. I’m not at all at that stage yet though. But any thoughts are welcome, if I am doing it right above or fail.

How about {:error, :upload_failed}. Changesets are very good in folding error messages for fields, which in your case might work out as well, but they don’t have means of holding error messages, which are not specific to a certain field in the form.

1 Like

Sorry don’t understand what you suggest. Use {:error, :upload_failed} where? And process that tuple where?

There’s two parts to that. The public api of the context function and whatever is calling it (the controller in your case). The API of your context should return whatever you feel is needed to describe what happened. The controllers job is then to convert the result into stuff on the rendered website, which informs the user. Error tuples is one potential result your context function could have. If a caller is supposed to handle an error you’d not want to use raise.

1 Like