Create 1:1 association records at once (child requires parents)

Currently I’m doing it by

defmodule User do
  ...

  schema "users" do
    has_one :credentials, Credential
  end

  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [])
    |> validate_required([])
  end
end

defmodule Credential do
  ...
  schema "credentials" do
    belongs_to :user, User
  end

  def changeset(%Credential{} = credential, attrs) do
    credential
    |> cast(attrs, [:user_id])
    |> validate_required([:user_id])
    |> assoc_constraint(:user)
  end
end

def create_user(attrs) do
  credential_attrs = attrs["credential"]
  Ecto.Multi.new
  |> Ecto.Multi.run(:user, fn _ -> create_user(attrs) end)
  |> Ecto.Multi.run(:credential, fn %{user: user} -> create_credential(%{credential_attrs | user_id: user.id}) end)
  |> Repo.transaction()
  |> case do
    {:ok, %{user: user}} -> {:ok, user}
    {:error, _falied_step, %Ecto.Changeset{} = changeset, _} -> {:error, changeset}
  end
end

But I want to use cast_assoc like below.

defmodule User do
  ...

  schema "users" do
    has_one :credentials, Credential
  end

  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [])
    |> validate_required([])
    |> cast_assoc(:credential, required: true)
  end
end

defmodule Credential do
  ...
  schema "credentials" do
    belongs_to :user, User
  end

  def changeset(%Credential{} = credential, attrs) do
    credential
    |> cast(attrs, [:user_id])
    |> validate_required([:user_id])
    |> assoc_constraint(:user)
  end
end

def create_user(attrs) do
  %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
end

But it’s not possible because put_assoc creates associated record first then creates self at the end.

Is there any better way to create this kinds of related records?

1 Like

If this was copy and pasted you have a typo.

def changeset(%Credential{} = credential, attrs) do
  credentail # typo
  ...
end
1 Like

I removed :user_id from cast, validate_required args and it works.

defmodule User do
  ...

  schema "users" do
    has_one :credentials, Credential
  end

  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [])
    |> validate_required([])
    |> cast_assoc(:credential, required: true)
  end
end

defmodule Credential do
  ...
  schema "credentials" do
    belongs_to :user, User
  end

  def changeset(%Credential{} = credential, attrs) do
    credential
    |> cast(attrs, []) # Removed :user_id
    |> validate_required([]) # Removed :user_id
    |> assoc_constraint(:user)
  end
end

def create_user(attrs) do
  %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
end