The other day I was lurking in the elixir slack and I saw this:
Well sir, hold my Kombucha! Let’s see if we can get to the bottom of the difference between put_embed
, put_assoc
, cast_embed
and cast_assoc
once and for all!
If you look at the docs for put_assoc
you will see this line:
Puts the given association entry or entries as a change in the changeset.
This function is used to work with associations as a whole.
And if you look at the docs for put_embed
you will see this line:
Puts the given embed entry or entries as a change in the changeset.
This function is used to work with embeds as a whole.
Now if you are anything like me you’ll stare at those two lines for a good few minutes thinking what the hell is the difference anyway. Then you’ll look at the name of the functions and realise your mistake.
In Ecto you can have two kinds of schemas, those backed by tables in a database and those that are not. The ones that are not are called embedded schemas, and you can tell them apart in the way that you define them. Non embedded schemas include the table name when you define them like this:
defmodule DinnerGuest do
use Ecto.Schema schema "dinner_guests" do
field(:name, :string)
end
end
This means there is a table in our database called “dinner_guests”, with a column called “name”. Embedded schemas are not backed by a database so they use a different function to define themselves called, wait for it, embedded_schema
:
defmodule DinnerGuest do
use Ecto.Schema embedded_schema do
field(:name, :string)
end
end
Both types of schemas can have relations to other schemas. Table backed ones have associations
, these are the has_one
, belongs_to
, has_many
etc that we know and love:
defmodule DinnerGuest do
use Ecto.Schema schema "dinner_guests" do
field(:name, :string)
has_one(:aurora_borealis, AuroraBorealis)
has_many(:steamed_hams, SteamedHam)
end
end
Somewhat confusingly embedded schemas (non table backed schemas) have embeds
, you can embeds_one
and embeds_many
other embedded schemas. They look like this:
defmodule DinnerGuest do
use Ecto.Schema embedded_schema do
field(:name, :string)
embeds_many(:steamed_hams, SteamedHam)
embeds_one(:aurora_borealis, AuroraBorealis)
end
end
So when we say an embedded_schema
we mean a schema which is not backed by a table. And when we say a schema has an embed, we mean it has an association with another schema which is not backed by a database table.
Now armed with this knowledge, let’s go back and consider what is the difference between put_assoc
and put_embed
? Well one works on associations (relations between table backed schemas) and the other on embeds (relations between non table backed schemas).
Okay awesome, so now we just have to figure out the difference between put
and cast
. To do that let’s imagine a client posts us some json data, we use the wonderful jason library to decode the body into an elixir map that looks like this:
data = %{
dinner_date: "2018-01-01",
name: "Super Nintendo Chalmers",
other: "Random data",
}
Now in our code we want to take that data, and do some things with it — maybe save it to a database or process it some other way. In order to do that we want to serialize it into a struct first. The simplest way we can do that is by defining a struct and calling the struct
function:
defmodule DinnerGuest do
defstruct [:name, :dinner_date] def new(data) do
struct(DinnerGuest, data)
end
end
This is good because the extra other
field in our data
that is not part of the struct definition is ignored, so if we pass it the data
above and we end up with a struct that looks like this:
%DinnerGuest{
name: "Super Nintendo Chalmers",
dinner_date: "2018-01-01"
}
But notice the problem? Our dinner_date
field is not a date, it is a string! What would be really cool is if we could define our struct such that we made it clear we wanted it to be an actual date. Well we can, using an ecto schema:
defmodule DinnerGuest do
use Ecto.Schema
embedded_schema do
field(:name, :string)
field(:dinner_date, :date)
end def new(data) do
struct(DinnerGuest, data)
end
end
This alone doesn’t change anything, if we call the new
function we will still get a DinnerGuest
with a string as a dinner_date
. But now instead of using the struct
function we can use changesets! If we do this:
Ecto.Changeset.cast(%DinnerGuest{}, data, [:name, :dinner_date])
|> Ecto.Changeset.apply_changes
This will return us:
%DinnerGuest{
name: "Super Nintendo Chalmers",
dinner_date: ~D[2018-01-01]
}
And our date is now an elixir Date
! The thing to take from this is thatEcto.Changeset.cast
takes the fields on the data and compares it to what the schema says it should be, then attempts to coerce it to that type if it can. In our case, ecto can convert our string to a date so we get the desired result.
So cast
casts the data. And put? put
does not. So putting this all together:
cast_embed
→ adds an embed as a change to a changeset, casting the data on the relation as it goes. Think relations between schemas that are not backed by tables, and think of strings of dates becoming dates.
put_embed
→ adds the embed as a change to the changeset, without casting any fields. Think of relations between schemas not backed by tables, and think data staying as it is → strings stay as strings, even if they should be dates.
cast_assoc
→ adds an association as a change to a changeset, casting the fieds to the types they should be as it goes. Think of table backed schemas, and think of strings of dates becoming dates.
put_assoc
→ adds an association as a change to a changeset, without casting any of the fields. Think of table backed schemas and think of date strings staying as date strings.
So how do we pick between them? First of all, is the realtion we are adding a table backed schema, or an embedded one? Then ask youself, can I trust that the data I am getting will be in the format I actually want — will dates be elixir dates already, will decimal
s be elixir decimals?
The final thing to know about all of these functions is that all of them work on the relation as a whole. To understand this, let’s look at an has_many
relationship. Our DinnerGuest
has_many
SteamedHam
s, meaning if our client posted us some data to add another steamed ham to a dinner guest, and we did this:
Ecto.Changeset.change(%DinneGuest{})
|> Ecto.Changeset.cast_assoc([%{"meat_type" => "medium rare"}])
all of the existing steamed_hams
would be removed and replaced by the single one we have provided. This is what it means to say it works with the association as a whole. Now sometimes this is great — we might want to replace all of the relations with whatever the user gave us. Other times we want to just add one more.
One way to do that is to first query for all of the steamed_hams
for that dinner guest, then add our new one to that list, and cast_assoc
the new plus the existing relations in one list:
existing_relations = [%{id: 1, meat_type: "rare"}]Ecto.Changeset.change(%DinneGuest{})
|> Ecto.Changeset.cast_assoc(
existing_relations ++ [%{"meat_type" => "medium rare"}]
)
But that would be a lot of work for something we can do much more simply. Instead we can just add to the SteamedHam
table, and associate the DinnerGuest
. To do that we could:
%SteamedHam{}
|> Ecto.Changeset.cast(data, [:meat_type])
|> Ecto.Changeset.put_assoc(:dinner_guest, dinner_guest)
|> Repo.insert!()
And there we have it. Hopefully this will help you remember the difference between putting casting assocs and embeds!
Use Ecto lots? Check out my other article on making casting data with Ecto a breeze and implementing sum types in ecto.