Match against the result of Ecto.Multi.*_all operations to avoid the transaction

For example, I have the next schema:

 @primary_key false
  schema "likes" do
    belongs_to :user, User
    belongs_to :post, Post

    timestamps()
  end

And I want to do the next transaction:

  def user_unlikes_post(user_id, post_id) do
    Multi.new
    |> Multi.delete_all(:like, (from Like, where: [user_id: ^user_id, post_id: ^post_id]))
    |> Multi.update_all(:decrement_likes_count, Post.decrement_likes(post_id), [])
    |> Repo.transaction
  end

I’d like to match the result of the delete_all and, if it is not exactly 1, stop the transaction.
I think I could do it with Multi.run but I’d like to know if there is an elegant way to perform this using only “data queries”.

Thanks for your time.

I think you will have to use Multi.run this in this case. In order to see the results of delete_all the query needs to actually execute, which only takes place when Repo.transaction is finally called. You will need to use run if you want to adjust an operation in your transaction based on the result of a previous operation.

1 Like

is arbitrary so Multi.run is made for this exact purpose of ‘arbitrary’ checks. I suppose a .delete might bark if there a 0 or multiple records - but that is very implicit, and I haven’t checked.

Also I would ‘normalize’ your DB and make sure that duplicate records never exists in the first place. If you add a DB constraints like:

ALTER TABLE likes ADD UNIQUE (user_id, post_id)

I would even suggest you add the pair as a primary key eg primary key (user_id, post_id) and then the unique constraint is not needed.

You gave me an idea, you can access to the transaction intermediate value as first argument to the function you pass to Multi.run/3. So at the moment I made this:

  def user_unlikes_post(user_id, post_id) do
    Multi.new
    |> Multi.delete_all(:delete_like, Like.destroy_like_query(user_id, post_id))
    |> Multi.run(:validation_delete_like, &check_delete_like/1)
    |> Multi.update_all(:decrement_likes_count, Post.decrement_likes(post_id), [])
    |> Repo.transaction
  end

  defp check_delete_like(%{delete_like: {0, _}}), do: {:error, :like_not_exists}
  defp check_delete_like(%{delete_like: {1, _}}), do: {:ok, nil}

But, IMHO ecto could have a method to make this simpler:

Multi.new
|> Multi.delete_all_assert(:delete_like, query, 1)

But maybe it is asking too much to the Multi API… or maybe, adding support for delete methods for composed keys.

I have it, but you still can delete without creating.

At the moment, I don’t have it as a primary key and i’m using unique_index with the constraint. Ecto does not allow deleting by composed key so this won’t solve my problem. Any additional reason to add it like composed primary key? I really don’t know :slight_smile: