A Cleaner Way to Organize Ecto Schema Fields
July 8th 2022
When you generate a new schema in a Phoenix project, you will get something like this:
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :favorite_color, :string
field :name, :string
field :total_pets, :integer
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :name, :favorite_color, :total_pets])
|> validate_required([:email, :name])
|> unique_constraint(:email)
end
end
The fields favorite_color
and total_pets
have been removed from validate_required
because our users can sign up without divulging that information. They are optional fields. In this fictional application, all we require is their email and name.
This code is fine, but there’s a little redundancy in the changeset function. The email and name fields are repeated in cast and validate_required
. Usually, you need to cast a field before it can be required. Also, it’s not immediately obvious that favorite_color
and total_pets
are optional. You need to compare the casted fields with the required fields.
That isn’t a problem when you only have 4 fields, but in a schema with 20 fields, it’s a mess.
I like to write schemas like this:
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
@required_fields ~w(email name)a
@optional_fields ~w(favorite_color total_pets)a
schema "users" do
field :email, :string
field :favorite_color, :string
field :name, :string
field :total_pets, :integer
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> unique_constraint(:email)
end
end
Now it’s explicit which fields are optional vs required and they’re listed clearly at the top of the module.