-
-
Notifications
You must be signed in to change notification settings - Fork 0
Лучшие практики
Рекомендации по эффективному использованию 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,Соответствуйте схеме БД:
// База данных: email VARCHAR NOT NULL
#[field(create, update, response)]
pub email: String,
// База данных: bio TEXT NULL
#[field(update, response)]
pub bio: Option<String>,// Пароли
#[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. Если нужны 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(())
}
}Используйте 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");
}
}#[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::*;// Неправильно: password_hash будет в Response!
pub struct User {
pub password_hash: String,
}
// Правильно
#[field(skip)]
pub password_hash: String,Если нужны связанные данные, используйте sql = "trait" и реализуйте самостоятельно.
Помните: поля UpdateRequest — это Option<T>. Проверяйте перед применением:
// Сгенерированный UpdateUserRequest имеет Option<String> для name
// Ваша логика обновления должна обрабатывать None (без изменений) vs Some (изменение)Помещайте валидацию и бизнес-правила в сервисный слой, не в обработчики:
// Хорошо: Сервисный слой
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-эндпоинтов
- Существуют индексы БД для паттернов запросов
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级