How to update both a changeset and it's association in a transaction

Hey I’m sure I’m just missing something obvious but I am struggling so hard trying to accomplish this.

To summarise I have two tables, a user table (with all the users details) and a user_verification table (contains a token I use to validate the account). I want to be able to update both the users email (in the user table) and the token (in the verification table) at the same time/in a single transaction.

put_assoc and cast_assoc have left me very confused, so I was no attempting to just get the user changeset with the user_verification field preloaded (working) and then use Ecto.Changeset.change to update the manually but I’m still not having any luck.

      user # has the user_verification already preloaded
      |> Ecto.Changeset.change(
        email: email,
        verified: false,
        user_verification:
          Map.replace!(
            user.user_verification,
            :token,
            :crypto.strong_rand_bytes(16) |> Base.url_encode64() |> String.slice(0, 16)
          )
      )

Would massively appreciate any help, have been on this wayyyy to long haha.

One way to do it is using Ecto.Multi

  Ecto.Multi.new 
  |> Ecto.Multi.update(:user, User.changeset(user, %{email: email, verified: false})) 
  |> Ecto.Multi.update(:user_verification, UserVerification.changeset(user.user_verification, %{token: token})) 
  |> Repo.transaction()
5 Likes

How have you defined the user_verification in your user schema?

Yep, my schema is:

  schema "users" do
    has_one(:user_verification, Accounts.UserVerification)

    field(:public_id, :string)
    field(:first_name, :string)
    field(:last_name, :string)
    field(:email, :string)
    field(:gender, :string)
    field(:date_of_birth, :date)
    field(:role, :string)
    field(:verified, :boolean, default: false, null: false)
    field(:removed_at, :utc_datetime)

    timestamps()
  end

and

  schema "user_verifications" do
    belongs_to(:user, Accounts.User)

    field(:token, :string)

    timestamps()
  end

I can successfully get the user and preload user_verification, I just don’t know how to go about updating them (the token field on user_verifications and email on user) in a sane way in one transaction

edit: just saw your solution @tspenov I’ll try it out now

1 Like

FWIW, it’s vastly more useful to include the error message you’re getting when “not having any luck”; it can be really useful for diagnosis.

That’s worth unpacking further, as I believe put_assoc will do what you’re looking for:

      new_user_verification =
        Map.replace!(
          user.user_verification,
          :token,
          :crypto.strong_rand_bytes(16) |> Base.url_encode64() |> String.slice(0, 16)
        )

      user
      |> Ecto.Changeset.change(email: email, verified: false)
      |> Ecto.Changeset.put_assoc(:user_verification, new_user_verification)

If you run this without making any changes, you’ll get an error message complaining that on_replace for the user_verification association is defaulted to raise. You’ll need to adjust the declaration in your User schema to include on_replace: :update for this to work.