I think Ecto should allow you to insert everything in one go and do some work for you. But if things get to complex or magical, remember that Repo.transaction
is your friend and you can always do things more manually. Something like:
Repo.transaction(fn ->
with {:ok, schema1} <- Schema1.create_changeset(params1) |> Repo.insert(),
{:ok, schema2} <- Schema2.create_changeset(schema1, params2) |> Repo.insert(),
# ...
# Transaction returns {:ok, schemaN}
schemaN
else
# Transaction returns {:error, changeset}
{:error, changeset} -> Repo.rollback(changeset)
end
end)
or if you want to make things composable, you can use Multi
:
Mulit.new()
|> Multi.insert(:schema1, Schema1.create_changeset(params1))
|> Multi.run(:schema2, fn repo, %{schema1: schema1} ->
schema1
|> Schema2.create_changeset(params2)
|> repo.insert()
end)
# ...
|> Repo.transaction()
|> case do
{:ok, %{schema1: schema1, ...}} -> {:ok, schemaN}
{:error, _failed_operation, failed_value, _changes_so_far} -> {:error, failed_value}
end
At this level things translate into straightforward SQL inserts/updates. If you go this way, I’d use pattern matching to destructure the params in the controller:
def create(conn, %{"schema1" => %{"attr1" => ..., "schema2" => %{ ... } = params2} = params1}) do
case Context.create_thing(params1, params2) do
# ...
end
end
The reason to do so would be to support params with both string keys (this is how data is coming in via Phoenix) and atom keys (more convenient to use in tests or IEx).