Skip to content

Лучшие практики

RAprogramm edited this page Jan 7, 2026 · 2 revisions

Рекомендации по эффективному использованию entity-derive в продакшене.

Проектирование сущностей

Сохраняйте фокус сущностей

Одна сущность на таблицу БД. Не пытайтесь моделировать сложные связи в одной сущности.

// Хорошо: Отдельные сущности
#[derive(Entity)]
#[entity(table = "users")]
pub struct User {
    #[id]
    pub id: Uuid,
    #[field(create, update, response)]
    pub name: String,
}

#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
    #[id]
    pub id: Uuid,
    #[field(create, response)]
    pub author_id: Uuid,  // Ссылка, не встраивание
    #[field(create, update, response)]
    pub title: String,
}

// Плохо: Попытка встроить связи
pub struct User {
    pub id: Uuid,
    pub posts: Vec<Post>,  // Не делайте так
}

Используйте осмысленные атрибуты полей

Будьте явными в отношении назначения каждого поля:

// Хорошо: Ясное намерение
#[field(create, response)]      // Задаётся один раз, всегда видно
pub email: String,

#[field(update, response)]      // Можно изменять, всегда видно
pub display_name: Option<String>,

#[field(response)]              // Только чтение, вычисляется/управляется извне
pub post_count: i64,

#[field(skip)]                  // Никогда не раскрывается
pub password_hash: String,

// Плохо: Всё везде
#[field(create, update, response)]  // Это действительно нужно для всех?
pub internal_id: String,

Предпочитайте Option для nullable полей

Соответствуйте схеме БД:

// База данных: email VARCHAR NOT NULL
#[field(create, update, response)]
pub email: String,

// База данных: bio TEXT NULL
#[field(update, response)]
pub bio: Option<String>,

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

Всегда используйте #[field(skip)] для конфиденциальных данных

// Пароли
#[field(skip)]
pub password_hash: String,

// API-ключи
#[field(skip)]
pub api_key: String,

// Внутренние токены
#[field(skip)]
pub refresh_token: Option<String>,

// Персональные данные, которые не должны быть в ответах
#[field(skip)]
pub ssn: String,

// Внутренние данные аудита
#[field(skip)]
pub created_by_ip: String,

Разделяйте внутренние и внешние сущности

Для данных только для администраторов рассмотрите отдельные сущности:

// Публичная сущность
#[derive(Entity)]
#[entity(table = "users")]
pub struct User {
    #[id]
    pub id: Uuid,
    #[field(create, update, response)]
    pub name: String,
    #[field(skip)]
    pub admin_notes: Option<String>,
}

// Сущность только для администраторов (та же таблица, другое представление)
#[derive(Entity)]
#[entity(table = "users", sql = "trait")]
pub struct AdminUser {
    #[id]
    pub id: Uuid,
    #[field(response)]
    pub name: String,
    #[field(update, response)]  // Теперь видно и редактируемо
    pub admin_notes: Option<String>,
    #[field(response)]
    pub last_login_ip: Option<String>,
}

Производительность

Используйте sql = "trait" для сложных запросов

Не боритесь со сгенерированным SQL. Если нужны JOIN или сложная логика, реализуйте самостоятельно:

// Простой CRUD - используйте полную генерацию
#[entity(table = "categories", sql = "full")]

// Нужны сложные запросы - реализуйте сами
#[entity(table = "posts", sql = "trait")]

Пакетные операции

Для массовых вставок реализуйте пользовательские методы:

#[entity(table = "events", sql = "trait")]
pub struct Event { /* ... */ }

pub trait EventBatchRepository {
    async fn create_batch(&self, events: Vec<CreateEventRequest>) -> Result<(), sqlx::Error>;
}

#[async_trait]
impl EventBatchRepository for PgPool {
    async fn create_batch(&self, events: Vec<CreateEventRequest>) -> Result<(), sqlx::Error> {
        let mut tx = self.begin().await?;

        for event in events {
            let entity = Event::from(event);
            let insertable = InsertableEvent::from(&entity);
            // Вставка внутри транзакции
        }

        tx.commit().await?;
        Ok(())
    }
}

Избегайте N+1 запросов

Используйте JOIN вместо поочерёдной загрузки связанных сущностей:

// Плохо: N+1 запросов
let posts = pool.list(100, 0).await?;
for post in &posts {
    let author = pool.find_user_by_id(post.author_id).await?;  // N запросов!
}

// Хорошо: Один запрос с JOIN
let posts_with_authors = pool.list_with_authors(100, 0).await?;  // 1 запрос

Тестирование

Используйте отдельную тестовую БД

#[cfg(test)]
mod tests {
    use sqlx::PgPool;

    async fn setup_test_db() -> PgPool {
        let url = std::env::var("TEST_DATABASE_URL")
            .expect("TEST_DATABASE_URL must be set");

        let pool = PgPool::connect(&url).await.unwrap();

        // Запуск миграций
        sqlx::migrate!("./migrations")
            .run(&pool)
            .await
            .unwrap();

        pool
    }

    #[tokio::test]
    async fn test_create_user() {
        let pool = setup_test_db().await;

        let request = CreateUserRequest {
            username: "test_user".into(),
            email: "[email protected]".into(),
        };

        let user = pool.create(request).await.unwrap();
        assert_eq!(user.username, "test_user");
    }
}

Тестируйте DTO отдельно

#[test]
fn test_user_response_excludes_password() {
    let user = User {
        id: Uuid::new_v4(),
        username: "test".into(),
        email: "[email protected]".into(),
        password_hash: "secret_hash".into(),
        created_at: Utc::now(),
    };

    let response = UserResponse::from(&user);

    // password_hash отсутствует в UserResponse
    assert_eq!(response.username, "test");
    // Нет способа получить password_hash через response
}

#[test]
fn test_update_request_is_partial() {
    let update = UpdateUserRequest {
        username: Some("new_name".into()),
        email: None,  // Не обновляем email
    };

    assert!(update.username.is_some());
    assert!(update.email.is_none());
}

Организация проекта

Рекомендуемая структура

src/
├── entities/           # Определения сущностей
│   ├── mod.rs
│   ├── user.rs
│   ├── post.rs
│   └── comment.rs
├── repositories/       # Расширения пользовательских репозиториев
│   ├── mod.rs
│   └── post_search.rs
├── handlers/           # HTTP-обработчики
│   ├── mod.rs
│   ├── users.rs
│   └── posts.rs
├── services/           # Бизнес-логика
│   ├── mod.rs
│   └── auth.rs
└── main.rs

Реэкспортируйте сгенерированные типы

// src/entities/mod.rs
mod user;
mod post;

pub use user::*;
pub use post::*;

Группируйте связанные сущности

// src/entities/auth/mod.rs
mod user;
mod session;
mod api_key;

pub use user::*;
pub use session::*;
pub use api_key::*;

Распространённые ошибки

1. Забыли #[field(skip)] для конфиденциальных полей

// Неправильно: password_hash будет в Response!
pub struct User {
    pub password_hash: String,
}

// Правильно
#[field(skip)]
pub password_hash: String,

2. Использование sql = "full" когда нужны JOIN

Если нужны связанные данные, используйте sql = "trait" и реализуйте самостоятельно.

3. Неправильная обработка опциональных обновлений

Помните: поля UpdateRequest — это Option<T>. Проверяйте перед применением:

// Сгенерированный UpdateUserRequest имеет Option<String> для name
// Ваша логика обновления должна обрабатывать None (без изменений) vs Some (изменение)

4. Дублирование бизнес-логики

Помещайте валидацию и бизнес-правила в сервисный слой, не в обработчики:

// Хорошо: Сервисный слой
impl UserService {
    pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, AppError> {
        self.validate_email(&request.email)?;
        self.check_username_available(&request.username).await?;
        self.pool.create(request).await.map_err(Into::into)
    }
}

// Плохо: Логика разбросана в обработчиках
pub async fn create_user(pool: State<PgPool>, request: Json<CreateUserRequest>) -> ... {
    // Валидация здесь
    // Бизнес-правила здесь
    // Вызов репозитория здесь
    // Всё перемешано
}

Чек-лист

Перед деплоем:

  • Все конфиденциальные поля имеют #[field(skip)]
  • DTO соответствуют ожиданиям API-контракта
  • Сложные запросы используют sql = "trait"
  • Интеграционные тесты покрывают методы репозитория
  • Обработка ошибок единообразна
  • Пагинация реализована для list-эндпоинтов
  • Существуют индексы БД для паттернов запросов

Clone this wiki locally