Claude
Skills
Sign in
Back

ecto-changesets

Included with Lifetime
$97 forever

Use when validating and casting data with Ecto changesets including field validation, constraints, nested changesets, and data transformation. Use for ensuring data integrity before database operations.

General

What this skill does


# Ecto Changesets

Master Ecto changesets to validate, cast, and transform data before database operations.
This skill covers changeset creation, validation, constraints, handling associations,
and advanced patterns for maintaining data integrity.

## Basic Changeset

```elixir
defmodule MyApp.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer

    timestamps()
  end

  def changeset(user, params \\ %{}) do
    user
    |> cast(params, [:name, :email, :age])
    |> validate_required([:name, :email])
  end
end

# Usage
changeset = MyApp.User.changeset(%MyApp.User{}, %{name: "John", email: "[email protected]"})
```

Changesets filter and validate parameters before they're applied to a struct.
The `cast/3` function specifies which fields can be changed, and `validate_required/2`
ensures specific fields are present.

## Creating and Validating Changesets

```elixir
defmodule MyApp.Person do
  use Ecto.Schema
  import Ecto.Changeset

  schema "people" do
    field :first_name, :string
    field :last_name, :string
    field :age, :integer

    timestamps()
  end

  def changeset(person, params \\ %{}) do
    person
    |> cast(params, [:first_name, :last_name, :age])
    |> validate_required([:first_name, :last_name])
    |> validate_number(:age, greater_than_or_equal_to: 0)
  end
end

# Create changeset
changeset = MyApp.Person.changeset(%MyApp.Person{}, %{first_name: "Jane"})

# Check validity
changeset.valid?  # false, last_name is missing

# Access errors
changeset.errors
# [first_name: {"can't be blank", [validation: :required]},
#  last_name: {"can't be blank", [validation: :required]}]
```

The `valid?` field indicates whether the changeset has any errors. The `errors`
field contains a keyword list of validation failures with error messages and metadata.

## Inserting with Changesets

```elixir
person = %MyApp.Person{}
changeset = MyApp.Person.changeset(person, %{
  first_name: "John",
  last_name: "Doe",
  age: 30
})

case MyApp.Repo.insert(changeset) do
  {:ok, person} ->
    # Successfully inserted
    IO.puts("Created person with ID: #{person.id}")

  {:error, changeset} ->
    # Validation or constraint errors
    IO.inspect(changeset.errors)
end
```

The `Repo.insert/1` function accepts a changeset and returns `{:ok, struct}` on
success or `{:error, changeset}` on failure. Pattern matching makes error handling
straightforward.

## Updating with Changesets

```elixir
person = MyApp.Repo.get!(MyApp.Person, 1)
changeset = MyApp.Person.changeset(person, %{age: 31})

case MyApp.Repo.update(changeset) do
  {:ok, updated_person} ->
    # Successfully updated
    IO.puts("Updated person age to: #{updated_person.age}")

  {:error, changeset} ->
    # Validation or constraint errors
    IO.inspect(changeset.errors)
end
```

Updates work similarly to inserts, but start with an existing struct from the
database. The changeset tracks which fields have changed.

## Type Casting

```elixir
changeset = Ecto.Changeset.cast(%MyApp.User{}, %{"age" => "25"}, [:age])
user = MyApp.Repo.insert!(changeset)
user.age  # 25 (integer, not string)
```

The `cast/3` function automatically converts parameter values to their schema-defined
types. Strings like "25" are converted to integers when the field type is `:integer`.

## Field Validations

```elixir
defmodule MyApp.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :username, :string
    field :age, :integer
    field :bio, :string
    field :website, :string

    timestamps()
  end

  def changeset(user, params \\ %{}) do
    user
    |> cast(params, [:email, :username, :age, :bio, :website])
    |> validate_required([:email, :username])
    |> validate_format(:email, ~r/@/)
    |> validate_length(:username, min: 3, max: 20)
    |> validate_length(:bio, max: 500)
    |> validate_number(:age, greater_than: 0, less_than: 150)
    |> validate_inclusion(:age, 18..100)
    |> validate_format(:website, ~r/^https?:\/\//)
  end
end
```

Ecto provides many built-in validators including `validate_format/3` for regex
patterns, `validate_length/3` for string lengths, `validate_number/3` for numeric
constraints, and `validate_inclusion/3` for allowed values.

## Custom Validation Functions

```elixir
defmodule MyApp.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true
    field :password_hash, :string

    timestamps()
  end

  def changeset(user, params \\ %{}) do
    user
    |> cast(params, [:email, :password])
    |> validate_required([:email, :password])
    |> validate_email_format()
    |> validate_password_strength()
    |> hash_password()
  end

  defp validate_email_format(changeset) do
    changeset
    |> validate_format(:email, ~r/@/, message: "must be a valid email")
    |> validate_length(:email, max: 255)
  end

  defp validate_password_strength(changeset) do
    validate_change(changeset, :password, fn :password, password ->
      cond do
        String.length(password) < 8 ->
          [password: "must be at least 8 characters"]

        not String.match?(password, ~r/[A-Z]/) ->
          [password: "must contain at least one uppercase letter"]

        not String.match?(password, ~r/[0-9]/) ->
          [password: "must contain at least one number"]

        true ->
          []
      end
    end)
  end

  defp hash_password(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
        put_change(changeset, :password_hash, hash_password_value(password))

      _ ->
        changeset
    end
  end

  defp hash_password_value(password) do
    # Use a real hashing library like Argon2 or Bcrypt
    :crypto.hash(:sha256, password) |> Base.encode64()
  end
end
```

Custom validation functions use `validate_change/3` to add custom logic. The
`put_change/3` function modifies changeset values, useful for transformations
like password hashing.

## Constraint Validations

```elixir
defmodule MyApp.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :username, :string

    timestamps()
  end

  def changeset(user, params \\ %{}) do
    user
    |> cast(params, [:email, :username])
    |> validate_required([:email, :username])
    |> unique_constraint(:email)
    |> unique_constraint(:username)
  end
end

# Usage
case MyApp.Repo.insert(changeset) do
  {:ok, user} ->
    # Success

  {:error, changeset} ->
    # Will contain unique constraint error if email/username exists
    changeset.errors
end
```

Constraint validations check database-level constraints like unique indexes.
They only run when the changeset is inserted or updated, not during validation.

## Unique Constraint with Custom Error Message

```elixir
def changeset(user, params \\ %{}) do
  user
  |> cast(params, [:email])
  |> validate_required([:email])
  |> unique_constraint(:email,
       name: :users_email_index,
       message: "has already been taken")
end
```

The `unique_constraint/3` function accepts options to specify the constraint name
and customize the error message. This maps database constraint violations to
user-friendly errors.

## Foreign Key Constraints

```elixir
defmodule MyApp.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :body, :string
    belongs_to :post, MyApp.Post

    timestamps()
  end

  def changeset(comment, params \\ %{}) do
    comment
    |> cast(params, [:body, :post_id])
    |> validate_required([:body, :post_id])
    |> foreign_key_constraint(:post_id)
  end
end
```

The `foreign_key_constraint/3` function validates that foreign key relationships
are valid. If you try to create a comment with a non-existent post_id, the
constraint will catch it.

## Check Constraints

```elixir
def changeset(product, params \\ %{}) do
  product
  |> cast(p

Related in General