Ecto Changesets — put, cast, embeds and assocs. Remember the difference once and for all!

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 decimals 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 SteamedHams, 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

Adz

Written by

Adz

Software Dev at https://nested.com/

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade