Как работает Ecto и чем он отличается от ActiveRecord?

Ecto — это библиотека для Elixir, предназначенная для взаимодействия с базами данных. Она выполняет функции ORM (Object-Relational Mapping), но реализована в функциональном стиле, сохраняя при этом иммутабельность и декларативность. Основные компоненты: схемы, изменения (changesets), запросы (queries), миграции и репозитории (repositories).

Основные компоненты Ecto

1. Schemas (схемы)

Схемы в Ecto описывают структуру таблиц и отображают строки БД на Elixir-структуры (struct). Это модель, но она не содержит поведения, в отличие от ActiveRecord.

defmodule MyApp.User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
timestamps()
end
end

Таблица users будет отображаться на структуру %User{}.

2. Changesets

Changeset — это способ безопасно валидировать и кастить входящие данные перед вставкой/обновлением. Это основа для обработки форм и API.

def changeset(user, attrs) do
user
|> Ecto.Changeset.cast(attrs, \[:name, :email\])
|> Ecto.Changeset.validate_required(\[:name, :email\])
end

В отличие от ActiveRecord, где валидация встроена в модель, здесь это делается явно и функционально. Changeset не изменяет данные напрямую — он возвращает новое состояние.

3. Repo (репозиторий)

Ecto.Repo — интерфейс для общения с базой данных. Он оборачивает адаптер (например, Postgrex), предоставляет функции insert, update, get, delete, all, preload.

MyApp.Repo.insert(changeset)
MyApp.Repo.get(User, 1)

Это аналог ActiveRecord::Base, но разделён на внешний слой. Нет прямого вызова вроде User.save, всё идёт через Repo.

4. Query (запросы)

Запросы в Ecto строятся декларативно с помощью макроса from или через pipeline API. Они возвращают структуру Ecto.Query.

import Ecto.Query
query = from u in User, where: u.age > 18, select: u.name
MyApp.Repo.all(query)

Либо:

User
|> where(\[u\], u.age > 18)
|> select(\[u\], u.name)
|> Repo.all()

Запросы не выполняются сразу, они ленивы до вызова Repo.all/Repo.one и т.п.

5. Associations и preload

Ассоциации описываются через has_many, belongs_to, has_one. Для загрузки связанных данных используется preload.

schema "posts" do
field :title, :string
has_many :comments, Comment
end
Repo.preload(post, :comments)

Нет ленивой загрузки как в ActiveRecord (post.comments не сработает автоматически).

6. Миграции

Миграции работают через mix ecto.gen.migration, затем код миграции пишется вручную:

def change do
create table(:users) do
add :name, :string
add :email, :string
timestamps()
end
end

В отличие от ActiveRecord, миграции более декларативны и не привязаны к модели.

Отличия от ActiveRecord (Rails)

Характеристика Ecto ActiveRecord
Архитектура Разделение схем, логики, репозитория Всё объединено в класс модели
--- --- ---
Поведение Changeset, отдельные функции Методы модели
--- --- ---
Валидаторы Отдельно от схемы (в changeset) Внутри модели
--- --- ---
Иммутабельность Да (новые структуры) Нет (объекты изменяются)
--- --- ---
Query API Композиция (pipeline) DSL в стиле where, joins
--- --- ---
Загрузка связей Явная (preload) Неявная (lazy loading)
--- --- ---
Миграции Похожи, но декларативнее Императивные DSL
--- --- ---
Исполнение запросов Отдельно через Repo Прямо из модели (Model.find)
--- --- ---
Тестирование Более изолированное Зависит от базы
--- --- ---
Поток данных Без состояния (stateless) Состояние в объекте
--- --- ---

Дополнительно

  • Ecto поддерживает транзакции (Repo.transaction/1), ограничения (check_constraint, unique_constraint), кастомные поля, embedded_schema, сериализацию JSON.

  • Он используется не только с PostgreSQL, но и с MySQL, SQLite, MSSQL, MongoDB (через сторонние адаптеры).

  • Есть гибкий механизм логгирования запросов (telemetry, Ecto.LogEntry).

Пример вставки нового пользователя

attrs = %{"name" => "Jon", "email" => "jon@example.com"}
changeset = User.changeset(%User{}, attrs)
case Repo.insert(changeset) do
{:ok, user} -> IO.puts("Inserted: #{user.id}")
{:error, changeset} -> IO.inspect(changeset.errors)
end

Никаких .save, .update_attributes, .valid? — всё делается вручную, прозрачно и явно.

Таким образом, Ecto реализует ORM в духе функционального программирования, устраняя магию, повышая прозрачность кода и контроль над жизненным циклом данных.