Ecto changesets: change vs cast

There are two ways of making an Ecto changeset. You can use Ecto.Changeset.change/2 or you can use Ecto.Changeset.cast/3. (The final argument of each is optional).

When to use change vs cast

For internal data, use change. It's very convenient to use, but doesn't do any filtering or validations, so make sure the data is coming from a source you trust.

For external data use cast. It filters attributes and acts as a type cast. For example, data coming into a web server via a form will all be in string form, but cast will transform each attribute into the type specified by the struct's Ecto schema. E.g., a user's "age" might come in as the string "27" and be cast as the integer 27.

Using change

Change takes two attributes. First is the struct to be changed. The second (optional) struct is a map of the changes to be made. The return value is either a changeset or an error.

iex> manning = Repo.get!(Link, 4)
%Linkly.Link{
  __meta__: #Ecto.Schema.Metadata<:loaded, "links">,
  bookmarks: #Ecto.Association.NotLoaded<association :bookmarks is not loaded>,
  id: 4,
  inserted_at: ~N[2021-02-10 18:23:02],
  taggings: #Ecto.Association.NotLoaded<association :taggings is not loaded>,
  tags: #Ecto.Association.NotLoaded<association :tags is not loaded>,
  updated_at: ~N[2021-02-10 18:23:02],
  url: "https://manning.com",
  users: #Ecto.Association.NotLoaded<association :users is not loaded>
}
iex> cs = change(hn, %{url: "nostarch.com"})
#Ecto.Changeset<
  action: nil,
  changes: %{url: "nostarch.com"},
  errors: [],
  data: #Linkly.Link<>,
  valid?: true
>

To commit the change, just pass it as a parameter to Repo.insert/1 or in this case, for updating an existing entry, pass it into Repo.update/1.

Using cast

The syntax for cast is similar to change, but it takes a third argument of allowed changes. So call cast with:

  1. the struct to be created or updated (e.g. %User{})
  2. the map or keyword list of attributes to be updated
  3. the list of allowed attributes

For example, cast(user, update_map, [:username]), will create a changeset to update user, and that changeset will include only the update for username (if any) supplied in the update_map. The email change will be discarded since :email wasn't included in the allowed attributes list. This way we can filter which attributes can be updated from a given source.

iex> bob = Repo.get!(User, 2)
%Linkly.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  about: nil,
  birth_date: nil,
  email: "robert@example.com",
  id: 2,
  inserted_at: ~N[2021-02-10 18:16:25],
  updated_at: ~N[2021-02-10 18:16:25],
  username: "bob"
}
iex> cs = cast(bob, %{username: "rob", email: "robert@example.com"}, [:username])
#Ecto.Changeset<
  action: nil,
  changes: %{username: "rob"},
  errors: [],
  data: #Linkly.User<>,
  valid?: true
>

(associations not shown for brevity)

Back to index

No Comments