Convert Embedded Schema struct to persisted schema

Hi,

On this page there is an example of an embedded schema if you scroll down a bit to the “SignUp” part: Ecto.Schema — Ecto v3.11.1

It mentions the following:

The SignUp schema can be cast and validated with the help of the Ecto.Changeset module, and afterwards, you can copy its data to the Profile and Account structs that will be persisted to the database with the help of Ecto.Repo.

But it doesn’t show how to convert SignUp to Profile and Account once it is validated? Once I do SignUp.changeset/2 I get back a changeset that is validated etc… but then how would I pass it into Account and Profiles? Sorry if this is a basic question, I’m quite new to this.

Thanks

1 Like

Let’s say you have a changeset for %Signup{} in signup_changeset. You could write something like this:

  case Ecto.Changeset.apply_action(signup_changeset, :insert) do
    {:ok, signup} ->
      # use signup.email etc to populate Profile and
      #  Account records and save to the DB
    {:error, changeset} ->
      # do something to tell the caller about the errors in `changeset`
  end
2 Likes

Thanks for the reply - so would I do something like this for example:

case Ecto.Changeset.apply_action(signup_changeset, :insert) do
    {:ok, signup} ->
        Account.changeset(signup) |> Repo.insert()
        Profile.changeset(signup) |> Repo.insert()
    {:error, changeset} ->
      # do something to tell the caller about the errors in `changeset`
  end
1 Like

Maybe - you’d need to write Account.changeset and Profile.changeset to accept a Signup struct.

The biggest thing that code is missing is the connection between Profile and Account.

A more “textbook” approach would be to build the structs explicitly:

  account = %Account{email: signup.email}

  profile =
    %Profile{
      name: signup.name,
      age: signup.age,
      account: account
    }

  Repo.insert(profile)

This assumes that the validations on Signup are the same or stricter than the expectations for Profile and Account. That way it’s not necessary to use another round of changesets.

One place that assumption will fail is changeset functionality that depends on the database, like uniqueness validation or check_constraint. Those can’t be written in Signup! For those you’ll need to build a changeset from the data in signup and pass that to Repo.insert:

  profile =
    Profile.changeset(%Profile{}, %{
      name: signup.name,
      age: signup.age,
      account: %{email: signup.email}
    })

  case Repo.insert(profile) do
    {:ok, profile} ->
      # return the created profile

    {:error, changeset} ->
      # extract errors from changeset and apply them to signup_changeset

  end

and in Profile:

def changeset(data, params) do
  data
  |> cast(params, [:name, :age])
  |> cast_assoc(:account)
  # other validations to taste
end

and in Account:

def changeset(data, params) do
  data
  |> cast(params, [:email])
  |> unique_constraint(:email)
end

Yet a third way to do this would be to break creating the Account completely apart from the creation of Profile - for instance, if you wanted to add a new Profile for an existing Account by email.

2 Likes