Skip to content

Navigation Menu

Sign in
Sign up

Главная

github-actions[bot] edited this page Jul 5, 2026 · 4 revisions

entity-derive

Один макрос, чтобы править всеми

English Русский 한국어 Español 中文


Что это?

entity-derive — процедурный макрос Rust, который генерирует полноценный доменный слой из одного определения сущности. Не просто CRUD — архитектурный фреймворк с событиями, хуками, командами и типобезопасной фильтрацией.


Проблема

Типичный Rust backend на ~10 сущностей — это:

Компонент Строк кода Проблемы
DTO (Create, Update, Response) ~60 на сущность Ручная синхронизация, забытые поля
Repository trait + impl ~150 на сущность SQL-ошибки в runtime, copy-paste
Маппинг Entity ↔ DTO ~40 на сущность Утечки данных (password_hash в Response)
Валидация и хуки Разбросаны по сервисам Дублирование, нет единого места
События/аудит Отсутствуют или adhoc Нет истории изменений

Итого: ~2500 строк boilerplate для 10 сущностей. И каждое изменение схемы — ручная правка в 5+ местах.


Решение

#[derive(Entity)]
#[entity(table = "users", events, hooks, commands)]
#[command(Register)]
#[command(Deactivate, requires_id)]
pub struct User {
 #[id]
 pub id: Uuid,
 #[field(create, update, response)]
 #[filter(like)]
 pub email: String,
 #[field(skip)] // Никогда не утечёт в API
 pub password_hash: String,
 #[auto]
 #[field(response)]
 pub created_at: DateTime<Utc>,
}

15 строк → полный доменный слой:

  • CreateUserRequest, UpdateUserRequest, UserResponse
  • UserRepository с типобезопасным SQL
  • UserEvent::Created, Updated, Deleted
  • UserHooks для бизнес-логики
  • RegisterUser, DeactivateUser команды
  • UserQuery для фильтрации

Просто для начинающих

Минимальный пример — 10 строк:

#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
 #[id]
 pub id: Uuid,
 #[field(create, update, response)]
 pub title: String,
 #[field(create, update, response)]
 pub content: String,
}

Готово. У тебя есть:

  • CreatePostRequest, UpdatePostRequest, PostResponse
  • PostRepository с create(), find_by_id(), update(), delete(), list()
  • Типобезопасный SQL для PostgreSQL
  • Всё работает из коробки

Никакой магии. Открой cargo expand — увидишь ровно тот код, который написал бы сам. Только без ошибок и за секунды.


Почему события?

Проблема: В CRUD-приложениях нет истории. Кто изменил запись? Когда? Что было до этого? Аудит требует отдельной инфраструктуры и дисциплины.

Решение: #[entity(events)] генерирует типизированные события:

pub enum UserEvent {
 Created(User),
 Updated { id: Uuid, changes: UpdateUserRequest },
 Deleted(Uuid),
}

Что это даёт:

  • Аудит из коробки — подпишись на события, сохраняй в лог
  • Event Sourcing — можно восстановить состояние из истории событий
  • Интеграции — Kafka, WebSocket уведомления, инвалидация кэша
  • Отладка — полная история изменений каждой сущности

Почему хуки?

Проблема: Бизнес-логика размазана. Валидация email — в контроллере. Хэширование пароля — в сервисе. Отправка письма — в отдельном воркере. Где искать логику создания пользователя?

Решение: #[entity(hooks)] централизует lifecycle:

impl UserHooks for MyHooks {
 async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Error> {
 dto.email = dto.email.to_lowercase(); // Нормализация
 validate_email(&dto.email)?; // Валидация
 Ok(())
 }
 async fn after_create(&self, user: &User) -> Result<(), Error> {
 self.mailer.send_welcome(user).await?; // Бизнес-действие
 Ok(())
 }
}

Что это даёт:

  • Единое место — вся логика сущности рядом с определением
  • Предсказуемость — ясно когда что выполняется
  • Тестируемость — хуки можно мокать и тестировать изолированно
  • Композиция — разные реализации для разных контекстов

Почему команды?

Проблема: REST API скрывает намерение. POST /users — это регистрация? Создание админом? Импорт из CSV? PATCH /users/123 — деактивация? Смена email? Бан?

Решение: #[command(...)] выражает бизнес-домен:

#[command(Register)] // Самостоятельная регистрация
#[command(Invite)] // Приглашение админом
#[command(Deactivate, requires_id)] // Деактивация аккаунта
#[command(Ban, requires_id)] // Бан за нарушения

Сравни:

// CRUD (что происходит?)
pool.update(user_id, UpdateUserRequest { active: Some(false), ..default() }).await?;
// Команды (ясное намерение)
handler.handle(DeactivateUser { id: user_id }).await?;

Что это даёт:

  • Самодокументируемый API — названия команд = бизнес-словарь
  • Разная логикаDeactivate и Ban могут иметь разные side-effects
  • CQRS готовность — команды легко маршрутизировать, логировать, ретраить
  • Типобезопасность — компилятор проверяет что команда существует

Почему типобезопасная фильтрация?

Проблема: Строковые query params — источник runtime-ошибок:

GET /users?stauts=active // Опечатка — молча игнорируется
GET /users?created_at=tomorrow // Невалидная дата — panic в runtime

Решение: #[filter] генерирует типизированную структуру:

let query = UserQuery {
 email: Some("@company.com".into()), // ILIKE '%@company.com%'
 created_at_min: Some(week_ago), // >= week_ago
 created_at_max: Some(now), // <= now
 ..Default::default()
};
let users = pool.list_filtered(&query, 100, 0).await?;

Что это даёт:

  • Compile-time проверка — опечатка в имени поля = ошибка компиляции
  • ТипобезопасностьDateTime нельзя сравнить со String
  • Автодополнение — IDE подсказывает доступные фильтры
  • SQL-injection защита — параметры биндятся, не конкатенируются

Прозрачность

Макрос не прячет логику. Всё что генерируется — это обычный Rust-код который ты можешь:

  • Прочитатьcargo expand показывает весь сгенерированный код
  • Понять — никаких runtime-рефлексий, только структуры и трейты
  • Переопределитьsql = "trait" и пиши свой SQL
  • Отладить — ошибки компилятора указывают на твой код, не на внутренности макроса
// Хочешь понять что генерируется?
cargo expand --lib | grep -A 50 "impl UserRepository"

Zero magic. Если макрос сломается — ты всегда можешь написать код руками. Он не создаёт lock-in.


Вся мощь Rust

Compile-time гарантии

#[field(skip)]
pub password_hash: String,

Это не runtime-проверка "не сериализуй это поле". Это физическое отсутствие поля в структуре UserResponse. Невозможно случайно вернуть — поля просто нет.

Zero-cost abstractions

Сгенерированный код — это:

  • Обычные struct без Box/dyn
  • Прямые вызовы sqlx без промежуточных слоёв
  • #[inline] на горячих путях
  • Никаких аллокаций сверх необходимого

Benchmark: сгенерированный репозиторий работает с той же скоростью, что и написанный вручную. Потому что это и есть тот же код.

Async из коробки

// Всё async, всё Send + Sync
let user = pool.find_by_id(id).await?;
let users = pool.list(100, 0).await?;

Полная совместимость с tokio, async-std, любым async runtime.

Строгая типизация

// Ошибка компиляции: нет такого поля
let query = UserQuery { naem: "test".into(), ..default() };
 ^^^^ unknown field
// Ошибка компиляции: неверный тип
let query = UserQuery { created_at_min: "yesterday".into(), ..default() };
 ^^^^^^^^^^^^ expected DateTime<Utc>

Если код скомпилировался — он работает правильно.


Профессиональная архитектура

Clean Architecture готовность

Domain Layer (entity-derive)
├── Entities — #[derive(Entity)]
├── DTOs — CreateRequest, UpdateRequest, Response
├── Repository Trait — абстракция хранилища
├── Events — доменные события
├── Commands — бизнес-операции
└── Hooks — lifecycle логика
Infrastructure Layer (твой код)
├── Repository Impl — PgPool автоматически или свой
├── Event Handlers — подписка на события
├── Command Handlers — реализация бизнес-логики
└── External Services — интеграции

Чистое разделение. Domain не знает про HTTP, базу данных, Kafka. Всё это — детали реализации.

CQRS/Event Sourcing готовность

// Command side
handler.handle(RegisterUser { email, name }).await?;
// Query side
let users = pool.list_filtered(&query, 100, 0).await?;
// Event side
match event {
 UserEvent::Created(user) => kafka.send("user.created", &user).await?,
 UserEvent::Updated { id, changes } => audit_log.record(id, changes).await?,
 _ => {}
}

Хочешь простой CRUD? Получи. Хочешь полноценный CQRS? Включи commands и events. Архитектура растёт вместе с проектом.

Расширяемость

Уровень 1: Базовый CRUD

#[entity(table = "users")]

Уровень 2: + Фильтрация

#[entity(table = "users")]
// + #[filter] на полях

Уровень 3: + События и хуки

#[entity(table = "users", events, hooks)]

Уровень 4: + CQRS команды

#[entity(table = "users", events, hooks, commands)]
#[command(Register)]
#[command(Deactivate, requires_id)]

Уровень 5: Полный контроль

#[entity(table = "users", sql = "trait", events, hooks, commands)]
// Свой SQL, своя логика, но с сохранением всех DTO и типов

Начни просто. Добавляй фичи по мере роста. Не переписывай — расширяй.


Безопасность

#[field(skip)]
pub password_hash: String,

skip означает: это поле никогда не появится в:

  • CreateUserRequest (нельзя передать извне)
  • UpdateUserRequest (нельзя изменить через API)
  • UserResponse (нельзя случайно вернуть клиенту)

Единственный способ работать с password_hash — напрямую через entity в коде. Утечка невозможна по дизайну.


Почему это круто

Аспект Что получаешь
Скорость разработки 10 сущностей за час вместо дня
Надёжность Compile-time проверка всего
Безопасность Невозможно случайно утечь данные
Производительность Zero-cost, как написанный вручную код
Понятность Прозрачная генерация, никакой магии
Гибкость От простого CRUD до CQRS за один атрибут
Масштабируемость Архитектура растёт с проектом
Поддерживаемость Один источник истины, меньше багов

Документация

Тема Описание
Атрибуты Полный справочник атрибутов
Фильтрация Типобезопасная фильтрация запросов
Связи belongs_to и has_many
События События жизненного цикла
Хуки Before/after хуки
Команды CQRS-паттерн
Кастомный SQL Сложные запросы
Примеры Реальные сценарии использования
Веб-фреймворки Интеграция с Axum, Actix
Лучшие практики Рекомендации для продакшена

Это не фреймворк который диктует как жить. Это инструмент который убирает рутину и даёт строить правильно.


🌐 Language

🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文


🇬🇧 English

Home

Getting Started

Features

Advanced


🇷🇺 Русский

Главная

Начало работы

Возможности

Продвинутое


🇰🇷 한국어

시작하기

기능

고급


🇪🇸 Español

Inicio

Comenzando

Características

Avanzado


🇨🇳 中文

首页

入门

功能

高级


📚 Reference

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /