पाठ 17 / 25

Ecto: Schemas, Changesets and Queries

Persist data with Ecto schemas, validate with changesets and query safely.

Data access, explicitly

Ecto is Elixir's database toolkit, not a traditional ORM. Its parts: the Repo (Shop.Repo) performs all database operations (Repo.insert, Repo.get, Repo.all, Repo.transaction); schemas map database tables to structs (schema "products" do field :name, :string ... end) with associations such as belongs_to and has_many; changesets filter, cast and validate external data before it touches the database (cast/3, validate_required, validate_number, unique_constraint), collecting errors to show to users; migrations (mix ecto.gen.migration add_products) evolve the schema with create table and indexes; and the query DSL builds composable, parameterised SQL (from p in Product, where: p.price < ^max), safe from SQL injection because values are pinned as parameters. Associations are never loaded implicitly: use Repo.preload or preload: in queries, which avoids hidden N+1 queries. Ecto.Multi groups several operations into a single transaction. PostgreSQL is the most common database, with adapters for MySQL, SQLite and others.

Schema, changeset, migration and queries

Validation happens in the changeset; queries pin values as parameters.

# priv/repo/migrations/20261003120000_create_products.exs
defmodule Shop.Repo.Migrations.CreateProducts do
  use Ecto.Migration

  def change do
    create table(:products) do
      add :sku, :string, null: false
      add :name, :string, null: false
      add :price_paise, :integer, null: false
      timestamps()
    end

    create unique_index(:products, [:sku])
  end
end

# lib/shop/catalog/product.ex
defmodule Shop.Catalog.Product do
  use Ecto.Schema
  import Ecto.Changeset

  schema "products" do
    field :sku, :string
    field :name, :string
    field :price_paise, :integer
    timestamps()
  end

  def changeset(product, attrs) do
    product
    |> cast(attrs, [:sku, :name, :price_paise])
    |> validate_required([:sku, :name, :price_paise])
    |> validate_number(:price_paise, greater_than: 0)
    |> unique_constraint(:sku)
  end
end

# lib/shop/catalog.ex (context)
defmodule Shop.Catalog do
  import Ecto.Query
  alias Shop.Repo
  alias Shop.Catalog.Product

  def create_product(attrs), do: %Product{} |> Product.changeset(attrs) |> Repo.insert()

  def cheaper_than(max_paise) do
    from(p in Product, where: p.price_paise < ^max_paise, order_by: [asc: p.price_paise])
    |> Repo.all()
  end
end

# Shop.Catalog.create_product(%{"sku" => "pen", "name" => "Pen", "price_paise" => 0})
# => {:error, %Ecto.Changeset{errors: [price_paise: {"must be greater than %{number}", ...}]}}

A security check at the door

A changeset is the security check at the entrance: it inspects every bag (field), lets through only what is allowed, and writes down everything wrong, so problems are reported politely instead of causing chaos inside the database.

त्वरित जाँच: What is the main job of an Ecto changeset?

  • To open database connections
  • To cast and validate external data and track changes and errors before persisting
  • To render HTML
  • To start processes
Answer

To cast and validate external data and track changes and errors before persisting — Changesets filter, cast and validate data, collecting errors.