View Source Embedded Schemas

Embedded schemas allow you to define and validate structured data. This data can live in memory, or can be stored in the database. Some use cases for embedded schemas include:

  • You are maintaining intermediate-state data, like when UI form fields map onto multiple tables in a database.

  • You are working within a persisted parent schema and you want to embed data that is...

    • simple, like a map of user preferences inside a User schema.
    • changes often, like a list of product images with associated structured data inside a Product schema.
    • requires complex tracking and validation, like an Address schema inside a User schema.
  • You are using a document storage database and you want to interact with and manipulate embedded documents.

User Profile Example

Let's explore an example where we have a User and want to store "profile" information about them. The data we want to store is UI-dependent information which is likely to change over time alongside changes in the UI. Also, this data is not necessarily important enough to warrant new User fields in the User schema, as it is not data that is fundamental to the User. An embedded schema is a good solution for this kind of data.

defmodule User do
  use Ecto.Schema

  schema "users" do
    field :full_name, :string
    field :email, :string
    field :avatar_url, :string
    field :confirmed_at, :naive_datetime

    embeds_one :profile, Profile do
      field :online, :boolean
      field :dark_mode, :boolean
      field :visibility, Ecto.Enum, values: [:public, :private, :friends_only]
    end

    timestamps()
  end
end

Embeds

There are two ways to represent embedded data within a schema, Ecto.Schema.embeds_many/3, which creates a list of embeds, and Ecto.Schema.embeds_one/3, which creates only a single instance of the embed. Your choice here affects the behavior of embed-specific functions like Ecto.Changeset.put_embed/4 and Ecto.Changeset.cast_embed/3, so choose whichever is most appropriate to your use case. In our example we are going to use Ecto.Schema.embeds_one/3 since users will only ever have one profile associated with them.

defmodule User do
  use Ecto.Schema

  schema "users" do
    field :full_name, :string
    field :email, :string
    field :avatar_url, :string
    field :confirmed_at, :naive_datetime

    embeds_one :profile, Profile do
      field :online, :boolean
      field :dark_mode, :boolean
      field :visibility, Ecto.Enum, values: [:public, :private, :friends_only]
    end

    timestamps()
  end
end

Embedded schemas defined in such way are said to be defined inline, which means that they are:

  • generated as a module in the parent scope with the appropriate struct (for the example above, the module will be User.Profile)
  • persisted within the parent schema
  • required to provide the with option to Ecto.Changeset.cast_embed/3

Extracting the embeds

While the above User schema is simple and sufficient, we might want to work independently with the embedded profile struct. For example, if there was a lot of functionality devoted solely to manipulating the profile data, we'd want to consider extracting the embedded schema into its own module. This can be achieved with Ecto.Schema.embedded_schema/1.

# user/user.ex
defmodule User do
  use Ecto.Schema

  schema "users" do
    field :full_name, :string
    field :email, :string
    field :avatar_url, :string
    field :confirmed_at, :naive_datetime

    embeds_one :profile, UserProfile

    timestamps()
  end
end

# user/user_profile.ex
defmodule UserProfile do
  use Ecto.Schema

  embedded_schema do
    field :online, :boolean
    field :dark_mode, :boolean
    field :visibility, Ecto.Enum, values: [:public, :private, :friends_only]
  end
end

Embedded schemas defined in such way are said to be explicit-defined, which:

  • are dedicated modules having own scope, changeset functions, props, documentation, etc...
  • could be embedded by multiple parent schemas
  • are persistence agnostic, which means that embedded_schema doesn't require to be persisted

It is important to remember that embedded_schema has many use cases independent of embeds_one and embeds_many. As they are persistent agnostic, they are ideal for scenarios where you want to manage structured data without necessarily persisting it. For example, if you want to build a contact form, you still want to parse and validate the data, but the data is likely not persisted anywhere. Instead, it is used to send an email. Embedded schemas would be a good fit for such a use case.

Migrations

If you wish to save your embedded schema to the database, you need to write a migration to include the embedded data.

alter table("users") do
  add :profile, :map
end

Whether you use embeds_one or embeds_many, it is recommended to use the :map data type (although {:array, :map} will work with embeds_many as well). The reason is that typical relational databases are likely to represent a :map as JSON (or JSONB in Postgres), allowing Ecto adapter libraries more flexibility over how to efficiently store the data.

Changesets

Changeset functionality for embeds will allow you to enforce arbitrary validations on the data. You can define a changeset function for each module. For example, the UserProfile module could require the online and visibility fields to be present when generating a changeset.

defmodule UserProfile do
  # ...

  def changeset(%UserProfile{} = profile, attrs \\ %{}) do
    profile
    |> cast(attrs, [:online, :dark_mode, :visibility])
    |> validate_required([:online, :visibility])
  end
end

profile = %UserProfile{}
UserProfile.changeset(profile, %{online: true, visibility: :public})

Meanwhile, the User changeset function can require its own validations without worrying about the details of the UserProfile changes because it can pass that responsibility to UserProfile via cast_embed/3. A validation failure in an embed will cause the parent changeset to be invalid, even if the parent changeset itself had no errors.

defmodule User do
  # ...

  def changeset(user, attrs \\ %{}) do
    user
    |> cast(attrs, [:full_name, :email, :avatar_url])
    |> cast_embed(:profile, required: true)
  end
end

changeset = User.changeset(%User{}, %{profile: %{online: true}})
changeset.valid? # => false; "visibility can't be blank"
changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}})
changeset.valid? # => true

In situations where you have kept the embedded schema within the parent module, e.g., you have not extracted a UserProfile, you can still have custom changeset functions for the embedded data within the parent schema.

defmodule User do
  use Ecto.Schema

  schema "users" do
    field :full_name, :string
    field :email, :string
    field :avatar_url, :string
    field :confirmed_at, :naive_datetime

    embeds_one :profile, Profile do
      field :online, :boolean
      field :dark_mode, :boolean
      field :visibility, Ecto.Enum, values: [:public, :private, :friends_only]
    end

    timestamps()
  end

  def changeset(%User{} = user, attrs \\ %{}) do
    user
    |> cast(attrs, [:full_name, :email])
    |> cast_embed(:profile, required: true, with: &profile_changeset/2)
  end

  def profile_changeset(profile, attrs \\ %{}) do
    profile
    |> cast(attrs, [:online, :dark_mode, :visibility])
    |> validate_required([:online, :visibility])
  end
end

changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}})
changeset.valid? # => true

Querying embedded data

Once you have written embedded data to the database, you can use it in queries on the parent schema.

user_changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}})
{:ok, _user} = Repo.insert(user_changeset)

(Ecto.Query.from u in User, select: {u.profile["online"], u.profile["visibility"]}) |> Repo.one
# => {true, "public"}

(Ecto.Query.from u in User, select: u.profile, where: u.profile["visibility"] == ^:public) |> Repo.all
# => [
#  %UserProfile{
#    id: "...",
#    online: true,
#    dark_mode: nil,
#    visibility: :public
#  }
#]

In databases where :maps are stored as JSONB (like Postgres), Ecto constructs the appropriate jsonpath queries for you. More examples of embedded schema queries are documented in json_extract_path/2.