Postgres can store unstructured data such as arrays, json, and jsonb as of version 9.4. Ecto, Elixir’s database wrapper, provides first class support for serializing and deserializing Ecto structs and arrays into these native data types without sacrificing the expressiveness of Ecto models. Embedded records have all the things regular models have, such as structured fields, lifecycle callbacks, and changesets. Let’s look at how easy it is to embed structs into our Ecto models.

You can use embeds_one to embed a single struct in an Ecto model. The record you are embedding into must have a database field using the :map type for unstructured data. In Postgres this is the jsonb type under the hood.

defmodule MyApp . Repo . Migrations . CreateAccount do use Ecto . Migration def change do alter table ( :accounts ) do add :name , :string add :settings , :map end end end

Now you can define the model you are embedding into:

defmodule Account do use Ecto . Model schema "accounts" do field :name embeds_one :settings , Settings end end

And now, the define the record you have embedded in Account :

defmodule Settings do use Ecto . Model # embedded_schema is short for: # # @primary_key {:id, :binary_id, autogenerate: true} # schema "embedded Item" do # embedded_schema do field :email_signature field :send_emails , :boolean end end

Embedded records behave like typical associations, except setting and deleting embeds can only be done via changesets.

account = Repo . get! ( Account , 20 ) settings = % Settings { email_signature: "Josh Steiner" , send_emails: true } # You may want to move this to the model layer. # This is done here for ease of demonstration. changeset = account |> Ecto . Changeset . change |> Ecto . Changeset . put_change ( :settings , settings ) Repo . update! ( changeset )

This will automatically call the function changeset/2 in the embedded model (in this case, Settings ) when saving the parent record. This means embedded records automatically go through validations! You can modify the function that is called by passing the :on_cast option to embeds_one .

Embedded records are conveniently loaded with the parent record, so you don’t have to worry about joins or preloading:

account = Repo . get! ( Account , 20 ) account . settings #=> %Settings{...}

embeds_many behaves similarly to embeds_one , but allows you to store an array of Ecto structs. Under the hood, Postgres uses a combination of array columns with jsonb elements.

defmodule MyApp . Repo . Migrations . CreatePeople do use Ecto . Migration def change do alter table ( :people ) do add :name , :string # It is recommended to set the default value to an empty array. add :addresses , { :array , :map }, default: [] end end end

Now you can define your models:

defmodule Person do use Ecto . Model schema "people" do field :name embeds_many :addresses , Address end end defmodule Address do use Ecto . Model embedded_schema do field :street_name field :city field :state field :zip_code end end

Setting many to many fields is done with an array :

person = Repo . get! ( Person , 7 ) addresses = [ % Address { street_name: "20 Foobar Street" , city: "Boston" , state: "MA" , zip_code: "02111" }, % Address { street_name: "1 Finite Loop" , city: "Cupertino" , state: "CA" , % zip_code: "95014" }, ] changeset = person |> Ecto . Changeset . change |> Ecto . Changeset . put_change ( :addresses , addresses ) Repo . update! ( changeset )

You can now access these like a has_many :

person = Repo . get! ( Person , 5 ) person . addresses #=> [%Address{...}, %Address{...}]

As you’ve seen, embedding records is simple and comes with many of the powerful features of Ecto. It’s even easy to add fields to an embedded record without running migrations. These are some nice benefits, however they come with serious trade-offs worth considering.

When you use unstructured data, you lose some of the powerful relational features that a SQL database provides. For example, since a record can only be embedded in a single parent, you can’t model a many-to-many relationship with embedded records. You also can’t use database constraints on structure and uniqueness when storing in a JSON field. While you can add these constraints to your application code, it’s usually best to validate at the database level to ensure data integrity.