Creating multiple related records at once with Ecto

I know about Ecto.Multi but was just making sure I’m doing this correctly:

I have an organization table. When an organization is created, a calendar, forum, and wiki need to be created. There are calendar, forum, and wiki tables all with an organization_id column to relate it with the created organization. So the pseudo-code of essentially what I want would look like this:

def create_organization() do
  org = Repo.insert!(%Organization{})

  Repo.insert!(%Forum{organization_id: org.id})
  Repo.insert!(%Calendar{organization_id: org.id})
  Repo.insert!(%Wiki{organization_id: org.id})

  # Need to return the organization, but have to make sure
  # all of the other rows are created before returning
  
  org
end

So my questions are:

  1. Is this a sane approach?
  2. Are there any better ways to do this or is this just how to create related rows in another table with Ecto?

The reason I’m asking this question is because all of the examples I’ve seen with put_assoc, cast_assoc, and Ecto.Multi only deal with creating a single related row based off of the result of the first insert (the organization in my case). I’m creating multiple rows and I haven’t seen this in my searches so hopefully this will show up for people that look for it :slight_smile:

Ecto.Multi is certainly the way to go. It also doesn’t care what resources you create after each other. If you really want to be “non-specific” about the order of forum/calender/wiki within the ecto.multi you can look into Ecto.Multi.merge instead of Ecto.Multi.run.

To build associations you can also use Ecto.build_assoc(org, :forum) if the association is also part of your organisation schema.

2 Likes

I would do something like this. I’m not familiar with any shortcuts.

1 Like

Definitely agreed that Ecto.Multi is the way to go. It would look something like this:

def create_organization() do
  alias Ecto.Multi
  Multi.new
  |> Multi.insert(:organization, %Organization{})
  |> Multi.merge(fn %{organization: org} ->
    organization_relation_multi(org.id)
  end)
  |> Repo.transaction()
end

def organization_relation_multi(org_id) do
  alias Ecto.Multi
  Multi.new
  |> Multi.insert(:forum, %Forum{organization_id: org.id})
  |> Multi.insert(:calendar, %Calendar{organization_id: org.id})
  |> Multi.insert(:wiki, %Wiki{organization_id: org.id})
end

Then either all entries would fail to insert or all of them would be inserted together. I think you may know this but in your original example if the calendar failed to insert then you’d be left with an orphaned/invalid organization and forum.

10 Likes

My implementation was pretty darn close to yours, actually! I didn’t know about Multi.merge so I’ll definitely take a peek at that.

Thanks @axelson and @LostKobrakai!

1 Like

@axelson is there any reason you are aliasing Ecto.Multi in each function rather than once above both functions (e.g. runtime efficiency or readability) ?

Thanks for the example code! I found this helpful as well

2 Likes

Not really. I think I just did it because:

  • I didn’t want to write out the example containing module
  • I didn’t want to write Ecto.Multi every time
  • But I also wanted it to be obvious that I was referring to Ecto.Multi so that people following along wouldn’t have compilation errors by just referring to Multi in their code
3 Likes