Using result of intermediate insert in transaction with Ecto Multi

Using Ecto v2.2.6, Phoenix 1.3

I have a blog app. When a user makes a Post, it inserts into the Post table, then it get the resulting id of that post and inserts that into a Newsfeeditem table. Ideally, I would like for this to happen as a transaction.

(I am using Absinthe graphql, so my return for the insert must be of the form {:ok, post})

I have a working function that looks like this:

  def create_post_add_newsfeed(%{
      title: title,
      content: content,
      user_id: user_id
    }) do

    case Repo.insert!(%Post{title: title, content: content, user_id: user_id}) do
      post ->
        case Repo.insert!(%Newsfeeditem{type: "user_creates_post", user_id: user_id, post_id: post.id}) do
          newsfeeditem ->
            {:ok, post}
          _ ->
            {:error, "Post not recorded in newsfeed"}
        end
      _ ->
      {:error, "Post not inserted"}
    end
  end

This code is not a transaction, and it reeks of callback stink. Ecto.Multi seems like a more appropriate tool to use here, but I do not know how to get the result of the Post insert so that I can insert it into Newsfeed.

I would like to do something like this

  def create_post_add_newsfeed(%{
      title: title,
      content: content,
      user_id: user_id
    }) do
    multi =
      Multi.new
        |> Multi.insert(:post, %Post{title: title, content: content, user_id: user_id})
        |> # Some intermediate step where I get the 'post' from the line above
        |> Multi.insert(:newsfeeditem, %Newsfeeditem{type: "user_creates_post", user_id: users_id, post_id: post.id})

    case Repo.transaction(multi) do
      {:ok, %{post: post}} ->
        {:ok, post}
      {:error, _} ->
        {:error, "Error"}
    end
  end

Any idea how to pull that off?

2 Likes

Have a look at https://hexdocs.pm/ecto/Ecto.Multi.html#run/3

This function receives the result of the previous Multi step as an argument. In this case you’d return the inserted Post from the step and intercept it.

The “whats-new-in-ecto-2.0.1” ebook contains a section doing just this (Composable transactions with Ecto.Multi) (http://pages.plataformatec.com.br/ebook-whats-new-in-ecto-2-0)

3 Likes

@cmkarlsson Thanks, that worked great!

I’ve run into a new issue with Multi.update. When I update a Post's content, it also adds a Newsfeed entry.

Here is the wrapper function:

  def updateContent(%{id: id, content: content}, _info) do
    post = Repo.get(post, id)
    Content.update_content_and_add_to_newsfeed(post, %{id: id, content: content})
  end

And here is the logic in the Content context:

  def update_content_and_add_to_newsfeed(post, %{id: id, content: content}) do
    multi =
      Multi.new
        |> Multi.update(:post, update_post(post, %{content: content}))
        |> Multi.insert(:newsfeed, %Newsfeed{message: "post updated"})

    case Repo.transaction(multi) do
      {:ok, %{post: post}} ->
        {:ok, post}
      {:error, _} ->
        {:error, "Error"}
    end
  end

When I run this code, the content updates in the database, but no newsfeed item gets inserted, and I see this error message in the console:

Server: localhost:4000 (http)
Request: POST /graphiql
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in Ecto.Multi.update/4

Any idea how to fix this?

What does update_post function do? Is it possible that this one updates the database outside of the multi transaction? If so that would explain what you are seeing.

1 Like

Here it is:

  def update_post(%Post{} = post, attrs) do
    post
    |> Post.changeset(attrs)
    |> Repo.update()
  end

Yes, the Repo.update will update the database outside the transaction. The Multi.update/4 takes a changeset.

You could create a new function:

def post_changeset(%Post{} = post, attrs) do
    post
    |> Post.changeset(attrs)

and use this in the Multi transaction.

Multi.new
|> Multi.update(:post, post_changeset(post, %{content: "Content"})
|> Multi.insert(...)
...

NOTE: Has not been tested :slight_smile:

2 Likes

Got it. This is what worked:

|> Multi.update(:post, Post.changeset(post, %{content: content}))
1 Like

In my case I needed to follow the initial insertion with an insert_all operation. Multi.merge helped.

2 Likes