Как работает 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 в духе функционального программирования, устраняя магию, повышая прозрачность кода и контроль над жизненным циклом данных.