home / skills / thebushidocollective / han / ecto-schemas

This skill helps you model Elixir Ecto schemas and associations efficiently, enabling robust data structures, validations, and embedded documents.

npx playbooks add skill thebushidocollective/han --skill ecto-schemas

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.6 KB
---
name: ecto-schemas
user-invocable: false
description: Use when defining and working with Ecto schemas including field types, associations, and embedded schemas. Use when modeling database entities in Elixir.
allowed-tools:
  - Bash
  - Read
---

# Ecto Schemas

Master Ecto schema definitions to model your database entities effectively in Elixir applications.

## Basic Schema Definition

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

  schema "users" do
    field :email, :string
    field :name, :string
    field :age, :integer
    field :is_active, :boolean, default: true
    field :role, Ecto.Enum, values: [:user, :admin, :moderator]

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :name, :age, :is_active, :role])
    |> validate_required([:email, :name])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0)
    |> unique_constraint(:email)
  end
end
```

## Field Types

```elixir
schema "products" do
  # Basic types
  field :name, :string
  field :price, :decimal
  field :quantity, :integer
  field :is_available, :boolean
  field :rating, :float

  # Date and time
  field :released_on, :date
  field :sale_starts_at, :naive_datetime
  field :sale_ends_at, :utc_datetime

  # Complex types
  field :tags, {:array, :string}
  field :metadata, :map
  field :settings, {:map, :string}  # map with string values

  # Binary data
  field :image_data, :binary
  field :uuid, :binary_id

  timestamps()
end
```

## Associations

```elixir
defmodule MyApp.Blog.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :body, :string

    # Belongs to
    belongs_to :author, MyApp.Accounts.User

    # Has many
    has_many :comments, MyApp.Blog.Comment

    # Has one
    has_one :featured_image, MyApp.Blog.Image

    # Many to many
    many_to_many :tags, MyApp.Blog.Tag, join_through: "posts_tags"

    timestamps()
  end
end

defmodule MyApp.Blog.Comment do
  use Ecto.Schema

  schema "comments" do
    field :content, :string

    belongs_to :post, MyApp.Blog.Post
    belongs_to :user, MyApp.Accounts.User

    timestamps()
  end
end
```

## Embedded Schemas

```elixir
defmodule MyApp.Accounts.Address do
  use Ecto.Schema

  # Embedded schema - stored as JSON in parent
  embedded_schema do
    field :street, :string
    field :city, :string
    field :state, :string
    field :zip, :string
    field :country, :string, default: "US"
  end

  def changeset(address, attrs) do
    address
    |> cast(attrs, [:street, :city, :state, :zip, :country])
    |> validate_required([:street, :city, :state, :zip])
  end
end

defmodule MyApp.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string

    # Single embedded
    embeds_one :billing_address, MyApp.Accounts.Address, on_replace: :update

    # Multiple embedded
    embeds_many :shipping_addresses, MyApp.Accounts.Address, on_replace: :delete

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name])
    |> cast_embed(:billing_address)
    |> cast_embed(:shipping_addresses)
  end
end
```

## Virtual Fields

```elixir
defmodule MyApp.Accounts.User do
  use Ecto.Schema

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

    # Virtual fields - not persisted
    field :password, :string, virtual: true
    field :password_confirmation, :string, virtual: true
    field :full_name, :string, virtual: true

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password, :password_confirmation])
    |> validate_required([:email, :password])
    |> validate_confirmation(:password)
    |> hash_password()
  end

  defp hash_password(changeset) do
    case get_change(changeset, :password) do
      nil -> changeset
      password -> put_change(changeset, :password_hash, Bcrypt.hash_pwd_salt(password))
    end
  end
end
```

## When to Use This Skill

Use ecto-schemas when you need to:

- Define database table structures
- Model relationships between entities
- Create embedded documents
- Handle complex data types
- Implement virtual computed fields

## Best Practices

- Use meaningful schema module names
- Keep schemas focused on data structure
- Use embedded schemas for nested JSON
- Define changesets in the schema module
- Use Ecto.Enum for constrained values
- Add database constraints with unique_constraint

## Common Pitfalls

- Forgetting timestamps() in schemas
- Not handling on_replace for embeds
- Mixing business logic into schemas
- Forgetting to add associations on both sides
- Not using virtual fields for sensitive data
- Overusing embedded schemas when separate tables are better

Overview

This skill helps you define and work with Ecto schemas for modeling database entities in Elixir applications. It covers field types, associations, embedded schemas, virtual fields, and changeset patterns to validate and transform data. Use it to design clear, maintainable mappings between your Elixir structs and database or JSON storage.

How this skill works

The skill inspects schema declarations and provides guidance for declaring fields, types, timestamps, and UUIDs, and for setting defaults and virtual fields. It explains association macros (belongs_to, has_many, has_one, many_to_many) and how embedded_schema stores nested data as JSON. It also outlines changeset patterns for casting, validations, unique constraints, and handling embedded data changes.

When to use it

  • Defining database table structures and typed fields
  • Modeling relationships between entities (belongs_to, has_many, etc.)
  • Storing nested JSON-like data with embedded schemas
  • Adding virtual fields for transient or sensitive values (passwords, computed values)
  • Validating and transforming input with changesets before persistence

Best practices

  • Give schemas clear, domain-focused module names
  • Keep schemas focused on shape and validation; avoid business logic heavy code
  • Define changeset functions inside the schema module for cohesive validation
  • Use embedded schemas for nested JSON and set on_replace explicitly
  • Prefer Ecto.Enum for constrained values and add DB-level constraints (unique_constraint)

Example use cases

  • Modeling a users table with email, role (Ecto.Enum), timestamps, and password virtual fields
  • Building a blog with posts, comments, authors using belongs_to and has_many associations
  • Storing address objects as embeds_one/embeds_many inside a user JSON column
  • Implementing product records with arrays, maps, decimals, and binary UUIDs
  • Creating secure signup flows: cast password as virtual, validate confirmation, then hash into password_hash

FAQ

When should I use an embedded schema vs a separate table?

Use embedded schemas when data is tightly coupled to the parent and stored as JSON (no independent lifecycle). Use separate tables when the data needs querying, indexing, or independent associations.

How do I handle updates to embedded entries safely?

Set on_replace (for example :update or :delete) on embeds and use cast_embed in the parent changeset; validate required fields in the embedded changeset to avoid accidental data loss.