Search⌘ K
AI Features

Cast and Filter

Explore how to use casting and filtering with Ecto Changesets to safely transform and validate data from both internal and external sources. Understand the process of generating changesets with functions like change and cast, controlling parameters, type casting, and handling nil values to ensure data integrity before updating the database.

The following example inserts a new Artist record based on data supplied by the user:

Elixir
import Ecto.Changeset
params = %{name: "Gene Harris"}
changeset =
%Artist{} \
|> cast(params, [:name]) \
|> validate_required([:name])
case Repo.insert(changeset) do
{:ok, artist} -> IO.puts("Record for #{artist.name} was created.")
{:error, changeset} -> IO.inspect(changeset.errors)
end

As this example demonstrates, changesets help us with the entire life cycle of making a change, starting with raw data and ending with the operation succeeding or failing at the database level. Let’s now zoom in on each step.

The first step is to take the raw input data that we want to apply to the database and generate an Ecto.Changeset struct. We call this “casting and filtering” because we perform any needed type casting operations, like turning a string into an integer, and filter out any values we don’t want to use. We can do this two different ways, depending on where our data is coming from. We’ll look at both in the following sections.

Create changesets using internal data

If the data is internal to the application, we can create a changeset using the Ecto.Changeset.change function. Here’s how we would create a changeset that inserts a new Artist record:

Elixir
import Ecto.Changeset
changeset = change(%Artist{name: "Charlie Parker"})

The import statement makes all of the functions in Ecto.Changeset available to our code. We won’t include this in the rest of the examples for brevity.

The process of making changes to an existing record is similar, but instead of passing in a new struct, we use a record fetched from Repo.

Elixir
artist = Repo.get_by(Artist, name: "Bobby Hutcherson")
changeset = change(artist)

We can add the data we’d like to change as optional arguments to the change function. This is how we might change the name field to something more formal:

Elixir
artist = Repo.get_by(Artist, name: "Bobby Hutcherson")
changeset = change(artist, name: "Robert Hutcherson")

The changeset is just a data structure in memory—no communication with the database has happened yet. As seen previously with the Repository pattern, nothing happens with the database until we get Repo involved. If we wanted to commit the change, we’d need to call Repo.update(changeset) and check the result to see if it succeeded. Before we do that, we can peek at the changes that will apply. The changes field of our changeset tells us what’s going to be updated:

Elixir
changeset.changes
widget

We can also use the change function to add more changes to a changeset that’s already been created. Instead of passing in an Artist struct as the first argument, we can pass another changeset. Using the changeset value we created in the last code example, we could add the artist’s birth date to the list of items we’re going to update.

Elixir
changeset = change(changeset, birth_date: ~D[1941-01-27])

It’s also possible to add both changes into a single change call.

Elixir
artist = Repo.get_by(Artist, name: "Bobby Hutcherson")
changeset = change(artist, name: "Robert Hutcherson",
birth_date: ~D[1941-01-27])

In either case, calling changes will now show both of the values that we are updating.

Elixir
changeset.changes
widget

The data we’ve been using so far has been generated in our code. In most cases, however, the data we want to apply will be coming from outside of the controlled environment of our code—forms our application presents to end users, API calls, command-line parameters, CSVs, other data files, and so forth. Ecto provides the cast function for creating changesets to deal with this potentially unruly data.

Create changesets using external data

When working with data coming from external sources, it’s essential to be careful. The cast function plays a similar role to change, as it’s used to take raw data and return a Changeset struct, but it’s got a few extra features to help make sure we’re getting only the data that we want.

The cast function

The cast function has three required arguments. The first is the same as change:. It should be a data structure representing the record to which we want to apply our changes. This could be a new schema struct (for example, %Artist{}), a schema struct representing a record fetched from the database, or another changeset.

The second argument is a map containing raw data that we want to apply.

The third is a list of the parameters we’ll allow to be added to the changeset. It acts as a filter and will add only parameters specified in the list to the changeset. This will discard the rest.

Here is how we can create a changeset for a new Artist record using user-supplied parameters. In the following examples, we’ll use the params variable to represent values supplied by the user.

Elixir
# values provided by the user
params = %{"name" => "Charlie Parker", "birth_date" => "1920-08-29",
"instrument" => "alto sax"}
changeset = cast(%Artist{}, params, [:name, :birth_date])
changeset.changes
widget

Take a close look at the result of the changes call and see what the cast has done for us.

  • First, the instrument value provided in the params map does not appear in the changeset. We only specified name and birth_date in the list of allowed values, so Ecto dropped the instrument field for us. This can be useful when importing data from sources we don’t control. For example, if we were importing data from a CSV, there could be extra columns of data that we don’t need. This setting helps us get rid of them.

  • Second, the call to cast converted the birth_date value from the string "1920-08-29" to an Elixir Date struct. As the name suggests, the cast will perform typecasting when turning the raw input into a changeset. In this case, our Artist schema defined birth_date as the :date type, so Ecto parsed the string value into a Date when creating the changeset. This worked because we received the date in a standard format. If we got an unknown date format, Ecto would not cast it and the changeset would be invalid. We’ll talk more about validating changesets in the next section.

Handle nil values

By default, the cast function will treat the empty string ”” as nil when creating the changeset by default. However, there may be times when we want other values turned into nil as well. For example, when working with spreadsheets, we’ll often see data that looks like this:

Elixir
params = %{"name" => "Charlie Parker", "birth_date" => "NULL"}

Instead of getting an empty cell, we get the string "NULL". We can tell Ecto that we want to consider "NULL" a blank value by adding the empty_values option to cast:

Elixir
params = %{"name" => "Charlie Parker", "birth_date" => "NULL"}
changeset = cast(%Artist{}, params, [:name, :birth_date],
empty_values: ["", "NULL"])
changeset.changes
widget

By adding “NULL” to the empty_values option, we were able to treat the birth_date value as empty and Ecto dropped it from the list of changes. We can specify as many different values as we need, but we need to include ”” if we want to convert empty strings.

Try it yourself

You can try these queries in the following terminal.

Terminal 1
Terminal
Loading...