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.
Embedding a single struct with embeds_one
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{...}
Embedding multiple structs with embeds_many
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{...}]
Trade-Offs
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.