Introduction
During my journey learning Elixir, I discovered Ecto, a powerful database wrapper and query generator that enables seamless interaction with SQL databases. While often compared to ORMs like Entity Framework Core or Ruby on Rails' ActiveRecord, Ecto diverges by avoiding automatic state tracking, requiring developers to manage changes explicitly. This article explores Ecto's core concepts, including repositories, schemas, migrations, and CRUD operations. For advanced features, refer to Ecto’s official documentation.
What is Ecto?
Ecto is a database wrapper and query generator for Elixir, designed to work with relational databases like PostgreSQL and MySQL. It provides:
- Map database tables to Elixir structs (via schemas).
- Generate type-safe queries using Elixir syntax.
- Validate data before persistence (via changesets).
- Manage schema evolution with version-controlled migrations .
Why Ecto Isn’t a Traditional ORM
Ecto avoids the pitfalls of traditional ORMs by:
- No Automatic State Tracking: Unlike ORMs, Ecto does not track entity states (e.g., dirty or changed fields).
- Explicit Data Flow: Developers must manually pass data through changesets before persistence.
- Functional Paradigm: Ecto aligns with Elixir’s functional programming model, avoiding the "impedance mismatch" of object-relational mapping.
Requirement
- Elixir 1.18+ (adjust for older versions if needed)
- Dependencies in
mix.exs
{:ecto_sql, "~> 3.0"}, # Ecto
{:postgrex, ">= 0.0.0"} # PostgreSQL driver
Setting Up a Repository
A repository (or repo) is Ecto's interface to the database, similar to Entity Framework's DbContext
.
Manual Setup
- Define a repo module:
defmodule Friends.Repo do
use Ecto.Repo,
otp_app: :friend,
adapter: Ecto.Adapters.Postgres
end
- Configure in
config/config.exs
:
config :friends, Friends.Repo,
database: "friends",
username: "user",
password: "pass",
hostname: "localhost"
Automatic Setup via Mix Task
Run:
mix ecto.gen.repo -r Friends.Repo
Running Ecto at Startup
Add the repo to your application supervisor in lib/<app_name>/application.ex
:
def start(_type, _args) do
children = [Friends.Repo]
...
end
Then update the config/config.exs
config :friends, ecto_repos: [Friends.Repo]
Creating a Schema
A schema maps a database table to an Elixir struct. For example, a Person
schema:
defmodule Friends.Person do
use Ecto.Schema
import Ecto.Changeset
schema "people" do
field :first_name, :string
field :last_name, :string
field :age, :integer
end
def changeset(person, params \\ %{}) do
person
|> cast(params, [:first_name, :last_name, :age])
|> validate_required([:first_name, :last_name])
end
end
Migrations
Migrations define database schema changes incrementally.
Manual Migration
Create a file in priv/repo/migrations/<datetime>_create_people.exs
:
defmodule Friends.Repo.Migrations.CreatePeople do
use Ecto.Migration
def change do
create table(:people) do
add :first_name, :string
add :last_name, :string
add :age, :integer
end
end
end
Automatic Migration via Mix Task
Run:
mix ecto.gen.migration create_people
This command will create and empty migration file.
defmodule Friends.Repo.Migrations.CreatePeople do
use Ecto.Migration
def change do
end
end
Execute the migration
Run:
mix ecto.create # create the your database
mix ecto.migrate # run your migrations
CRUD Operations
Create
Insert a new record:
person = %Friends.Person{first_name: "Alice", last_name: "Smith", age: 30}
{:ok, inserted_person} = Friends.Repo.insert(person)
With validation:
changeset = Friends.Person.changeset(%Friends.Person{}, %{first_name: "Alice"})
case Friends.Repo.insert(changeset) do
{:ok, person} -> # Success
{:error, changeset} -> # Handle errors
end
Read
Fetch records:
# By ID
Friends.Repo.get(Friends.Person, 1)
# First record
Friends.Repo.one(from p in Friends.Person, order_by: [asc: p.id], limit: 1)
# All records matching a condition
Friends.Repo.all(from p in Friends.Person, where: like(p.first_name, "A%"))
Update
Update an existing record:
person = Friends.Repo.get!(Friends.Person, 1)
changeset = Friends.Person.changeset(person, %{age: 31})
Friends.Repo.update(changeset)
Delete
Delete a record:
person = Friends.Repo.get!(Friends.Person, 1)
Friends.Repo.delete(person)
That method will return a similar value as creating, but change the action
to :delete
Conclusion
Ecto strikes a balance between abstraction and control, offering:
- Type-Safe Queries: Compile-time macro-generated queries prevent runtime errors.
- Explicit Workflows: Changesets enforce data validation before persistence.
- Community Backing: Widely adopted in the Elixir ecosystem.
While not a traditional ORM, Ecto’s functional design avoids leaky abstractions common in object-relational mappers. For advanced patterns (e.g., associations), explore its support for has_many
, belongs_to
, and many_to_many
relationships.
Top comments (0)