Inserting parent and associated model in one go

Hi,

I have two Ecto schema models as follows:

defmodule User do
  schema "users" do
    has_one :channel, Channel

    field :email, :string
    field :first_name, :string
    field :last_name, :string
    
    timestamps()
  end
end

defmodule Channel do
  use Ecto.Schema
  import Ecto.Changeset



  schema "channels" do
    field :strength, :integer
    belongs_to :user, User

    timestamps()
  end


In my App, there cannot exist a channel without a user, so the user_id field on channel is non-null, unique and references the user’s id.
When I insert a user, I would also like to insert a channel for him.
My current code looks as follows:

attrs = %{first_name: "blah", last_name: "blah", email: "blahhh.."}

 user =   %User{}
    |> User.changeset(attrs)
    |> Repo.insert()

  %Channel{}
  |> Channel.changeset(%{})
  |> Changeset.put_assoc(:user, user)
  |> Repo.insert()

I want to make this in one shot. I know there will always be two inserts but I’d like to find out if there’s a more elegant way to do this ( In Rails, there is a build( ) method for associated records to be called, and ActiveRecord takes care of it).
I have checked out put_assoc, cast_assoc and build_assoc but looks like they are not going to do the job for me.
Can someone help me with this?

thank you!

Looks to me like job for cast_assoc, I would make User owner of this relationship and in create_user_changeset use cast_assoc.

I can strongly recommend you Pragprog’s “Programming Ecto” book, even though its in beta, its a pretty solid resource already.

I found the answer, looks like we can use put_assoc here.
This got me confused as I always read online that we use “put_assoc” only when we have the associated record “ready”, although in my case the “user” is not really “ready”.
The code:

    %User{}
    |> User.changeset(user_attrs)
    |> Ecto.Changeset.put_assoc(:channel, Channel.changeset(%Channel{}))
    |> Repo.insert()

Have you looked at Ecto.Multi? It executes multiple actions as one transaction. If one fails, they are all rolled back.

2 Likes

The typical way to do this is with:

|> Ecto.Changeset.cast_assoc(:channel)

The only requirement is that the channel’s data be in your params passed to the initial cast.

1 Like

I’ve just discovered it. Seems to be powerful. Will check out. Thanks

Well I’m using all the defaults to create the channel so i don’t even need to put in an empty map, and putting an empty map just to make it work doesn’t seem all that elegant

1 Like

For note, this is absolutely the use case of Ecto.Multi, you can get race conditions otherwise without the transactional coherency.

2 Likes

Thank you, will keep that in mind