Skip to content

Базы данных (sea-orm) в Axum

Интеграция Axum с базами данных является важной частью разработки веб-приложений. В этом разделе мы рассмотрим, как использовать Sea-ORM — современный асинхронный ORM для Rust — в приложениях на Axum.

Содержание

Подключение Sea-ORM к проекту

Для начала добавьте необходимые зависимости в Cargo.toml:

toml
[dependencies]
axum = "0.7.2"
tokio = { version = "1.32.0", features = ["full"] }
sea-orm = { version = "0.12.3", features = [
    "sqlx-postgres",  # выберите необходимую базу данных
    "runtime-tokio-rustls",
    "macros",
    "with-chrono",
    "with-json",
] }

Настройка подключения к базе данных

Создание и настройка соединения с базой данных:

rust
use axum::{
    extract::State,
    http::StatusCode,
    routing::{get, post},
    Router,
};
use sea_orm::{Database, DatabaseConnection};
use std::sync::Arc;

// Функция для установки соединения с базой данных
async fn establish_connection() -> Result<DatabaseConnection, StatusCode> {
    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL должен быть установлен");
    
    Database::connect(database_url)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}

// Главная функция приложения
#[tokio::main]
async fn main() {
    // Инициализация логирования
    tracing_subscriber::fmt::init();
    
    // Подключение к базе данных
    let db = establish_connection()
        .await
        .expect("Не удалось подключиться к базе данных");
    
    // Оборачиваем соединение в Arc для совместного использования
    let db_state = Arc::new(db);
    
    // Создание маршрутизатора с состоянием БД
    let app = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/users/:id", get(get_user))
        .with_state(db_state);
    
    // Запуск сервера
    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Определение моделей данных

Создание Entity с помощью sea-orm-cli

Sea-ORM предоставляет инструмент командной строки для генерации кода моделей из существующей базы данных:

bash
# Установка sea-orm-cli
cargo install sea-orm-cli

# Генерация сущностей из существующей базы данных
sea-orm-cli generate entity \
    -u postgres://username:password@localhost/database \
    -o src/entity

Ручное определение Entity

rust
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

// Определение сущности пользователя
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    
    #[sea_orm(column_type = "Text", unique)]
    pub email: String,
    
    #[sea_orm(column_type = "Text")]
    pub name: String,
    
    #[sea_orm(column_type = "Text", nullable)]
    pub profile_image: Option<String>,
    
    #[sea_orm(column_type = "Timestamp", nullable)]
    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
    
    #[sea_orm(column_type = "Timestamp", nullable)]
    pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}

// Определение связей
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(has_many = "super::post::Entity")]
    Post,
}

impl Related<super::post::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::Post.def()
    }
}

// Реализация ActiveModelBehavior для настройки поведения
impl ActiveModelBehavior for ActiveModel {
    // Хуки жизненного цикла
    
    // Перед сохранением
    fn before_save(mut self, insert: bool) -> Result<Self, DbErr> {
        if insert {
            self.created_at = Set(Some(chrono::Utc::now()));
        }
        self.updated_at = Set(Some(chrono::Utc::now()));
        
        Ok(self)
    }
}

Базовые операции CRUD

Create (Создание)

rust
use sea_orm::{ActiveModelTrait, Set};
use entity::user;

// Создание нового пользователя
async fn create_user_in_db(
    db: &DatabaseConnection,
    email: String,
    name: String,
) -> Result<user::Model, DbErr> {
    // Создание ActiveModel
    let new_user = user::ActiveModel {
        email: Set(email),
        name: Set(name),
        profile_image: Set(None),
        ..Default::default() // Остальные поля будут установлены по умолчанию
    };
    
    // Сохранение в базе данных и возврат созданной модели
    new_user.insert(db).await
}

Read (Чтение)

rust
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait};
use entity::user;

// Получение пользователя по ID
async fn get_user_by_id(
    db: &DatabaseConnection,
    user_id: i32,
) -> Result<Option<user::Model>, DbErr> {
    user::Entity::find_by_id(user_id).one(db).await
}

// Получение всех пользователей
async fn get_all_users(
    db: &DatabaseConnection,
) -> Result<Vec<user::Model>, DbErr> {
    user::Entity::find().all(db).await
}

// Фильтрация пользователей
async fn find_users_by_name(
    db: &DatabaseConnection,
    name_contains: &str,
) -> Result<Vec<user::Model>, DbErr> {
    user::Entity::find()
        .filter(user::Column::Name.contains(name_contains))
        .all(db)
        .await
}

Update (Обновление)

rust
use sea_orm::{EntityTrait, Set, ActiveModelTrait};
use entity::user;

// Обновление существующего пользователя
async fn update_user_profile(
    db: &DatabaseConnection,
    user_id: i32,
    new_name: Option<String>,
    new_profile_image: Option<String>,
) -> Result<user::Model, DbErr> {
    // Сначала получаем модель
    let user = user::Entity::find_by_id(user_id)
        .one(db)
        .await?
        .ok_or_else(|| DbErr::Custom("Пользователь не найден".to_string()))?;
    
    // Создаем ActiveModel из модели
    let mut user_active: user::ActiveModel = user.into();
    
    // Обновляем только указанные поля
    if let Some(name) = new_name {
        user_active.name = Set(name);
    }
    
    user_active.profile_image = Set(new_profile_image);
    
    // Сохраняем изменения и возвращаем обновленную модель
    user_active.update(db).await
}

Delete (Удаление)

rust
use sea_orm::{EntityTrait, DeleteResult};
use entity::user;

// Удаление пользователя по ID
async fn delete_user(
    db: &DatabaseConnection,
    user_id: i32,
) -> Result<DeleteResult, DbErr> {
    user::Entity::delete_by_id(user_id).exec(db).await
}

Интеграция с Axum

Использование DatabaseConnection в обработчиках

rust
use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter};
use std::sync::Arc;
use entity::user;
use serde::{Deserialize, Serialize};

// Структура для создания пользователя
#[derive(Deserialize)]
struct CreateUserRequest {
    email: String,
    name: String,
}

// Обработчик для получения списка пользователей
async fn list_users(
    State(db): State<Arc<DatabaseConnection>>,
) -> Result<Json<Vec<user::Model>>, StatusCode> {
    let users = user::Entity::find()
        .all(&*db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok(Json(users))
}

// Обработчик для получения конкретного пользователя
async fn get_user(
    State(db): State<Arc<DatabaseConnection>>,
    Path(user_id): Path<i32>,
) -> Result<Json<user::Model>, StatusCode> {
    let user = user::Entity::find_by_id(user_id)
        .one(&*db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .ok_or(StatusCode::NOT_FOUND)?;
    
    Ok(Json(user))
}

// Обработчик для создания нового пользователя
async fn create_user(
    State(db): State<Arc<DatabaseConnection>>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<Json<user::Model>, StatusCode> {
    // Проверка уникальности email
    let existing_user = user::Entity::find()
        .filter(user::Column::Email.eq(&payload.email))
        .one(&*db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    if existing_user.is_some() {
        return Err(StatusCode::CONFLICT);
    }
    
    // Создание нового пользователя
    let new_user = user::ActiveModel {
        email: Set(payload.email),
        name: Set(payload.name),
        ..Default::default()
    };
    
    let user = new_user
        .insert(&*db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok(Json(user))
}

Создание репозитория для разделения бизнес-логики

rust
use sea_orm::{
    DatabaseConnection, EntityTrait, QueryFilter,
    Set, ActiveModelTrait, DbErr, DeleteResult,
};
use entity::user;

// Трейт репозитория для работы с пользователями
#[async_trait]
trait UserRepository {
    async fn find_by_id(&self, id: i32) -> Result<Option<user::Model>, DbErr>;
    async fn find_all(&self) -> Result<Vec<user::Model>, DbErr>;
    async fn create(&self, email: String, name: String) -> Result<user::Model, DbErr>;
    async fn update(&self, id: i32, name: Option<String>) -> Result<user::Model, DbErr>;
    async fn delete(&self, id: i32) -> Result<DeleteResult, DbErr>;
}

// Реализация репозитория
struct SeaOrmUserRepository {
    db: DatabaseConnection,
}

impl SeaOrmUserRepository {
    fn new(db: DatabaseConnection) -> Self {
        Self { db }
    }
}

#[async_trait]
impl UserRepository for SeaOrmUserRepository {
    async fn find_by_id(&self, id: i32) -> Result<Option<user::Model>, DbErr> {
        user::Entity::find_by_id(id).one(&self.db).await
    }
    
    async fn find_all(&self) -> Result<Vec<user::Model>, DbErr> {
        user::Entity::find().all(&self.db).await
    }
    
    async fn create(&self, email: String, name: String) -> Result<user::Model, DbErr> {
        let new_user = user::ActiveModel {
            email: Set(email),
            name: Set(name),
            ..Default::default()
        };
        
        new_user.insert(&self.db).await
    }
    
    async fn update(&self, id: i32, name: Option<String>) -> Result<user::Model, DbErr> {
        let user = self.find_by_id(id).await?
            .ok_or_else(|| DbErr::Custom("Пользователь не найден".to_string()))?;
        
        let mut user_active: user::ActiveModel = user.into();
        
        if let Some(name) = name {
            user_active.name = Set(name);
        }
        
        user_active.update(&self.db).await
    }
    
    async fn delete(&self, id: i32) -> Result<DeleteResult, DbErr> {
        user::Entity::delete_by_id(id).exec(&self.db).await
    }
}

Миграции

Sea-ORM поддерживает управление миграциями схемы базы данных через sea-orm-migration крейт.

Настройка миграций

Сначала добавим зависимости в Cargo.toml:

toml
[dependencies]
# ... другие зависимости
sea-orm-migration = "0.12.3"

Создание миграций

rust
use sea_orm_migration::prelude::*;

// Миграция для создания таблицы пользователей
#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // Создание таблицы
        manager
            .create_table(
                Table::create()
                    .table(Users::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Users::Id)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(
                        ColumnDef::new(Users::Email)
                            .string()
                            .not_null()
                            .unique_key(),
                    )
                    .col(ColumnDef::new(Users::Name).string().not_null())
                    .col(ColumnDef::new(Users::ProfileImage).string())
                    .col(ColumnDef::new(Users::CreatedAt).timestamp())
                    .col(ColumnDef::new(Users::UpdatedAt).timestamp())
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // Удаление таблицы при откате
        manager
            .drop_table(Table::drop().table(Users::Table).to_owned())
            .await
    }
}

// Определение столбцов
#[derive(Iden)]
enum Users {
    Table,
    Id,
    Email,
    Name,
    ProfileImage,
    CreatedAt,
    UpdatedAt,
}

Запуск миграций

rust
use sea_orm_migration::prelude::*;

// Функция для выполнения миграций
async fn run_migrations(db: &DatabaseConnection) -> Result<(), DbErr> {
    let schema_manager = SchemaManager::new(db);
    
    // Регистрируем все миграции
    Migrator::new(vec![
        Box::new(migrations::m20220101_000001_create_users::Migration),
        Box::new(migrations::m20220101_000002_create_posts::Migration),
        // ... другие миграции
    ])
    .run(db)
    .await
}

Транзакции

Sea-ORM поддерживает транзакции для атомарного выполнения нескольких операций:

rust
use sea_orm::{DatabaseConnection, DbErr, EntityTrait, TransactionTrait};
use entity::{user, post};

// Функция для создания пользователя и его первого поста в одной транзакции
async fn create_user_with_post(
    db: &DatabaseConnection,
    email: String,
    name: String,
    post_title: String,
    post_content: String,
) -> Result<(user::Model, post::Model), DbErr> {
    // Начинаем транзакцию
    let transaction = db.begin().await?;
    
    // Создаем пользователя
    let new_user = user::ActiveModel {
        email: Set(email),
        name: Set(name),
        ..Default::default()
    };
    
    let user = new_user.insert(&transaction).await?;
    
    // Создаем пост связанный с пользователем
    let new_post = post::ActiveModel {
        title: Set(post_title),
        content: Set(post_content),
        author_id: Set(user.id),
        ..Default::default()
    };
    
    let post = new_post.insert(&transaction).await?;
    
    // Если все операции успешны, фиксируем транзакцию
    transaction.commit().await?;
    
    Ok((user, post))
}

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

1. Создание пула соединений

rust
use sea_orm::{Database, DatabaseConnection, ConnectOptions};
use std::time::Duration;

async fn create_database_pool() -> Result<DatabaseConnection, DbErr> {
    let mut options = ConnectOptions::new(std::env::var("DATABASE_URL").unwrap());
    
    // Установка параметров соединения
    options
        .max_connections(32)
        .min_connections(5)
        .connect_timeout(Duration::from_secs(8))
        .acquire_timeout(Duration::from_secs(8))
        .idle_timeout(Duration::from_secs(8))
        .max_lifetime(Duration::from_secs(8))
        .sqlx_logging(true)
        .sqlx_logging_level(log::LevelFilter::Info);
    
    Database::connect(options).await
}

2. Абстракция бизнес-логики через репозитории

rust
use std::sync::Arc;
use async_trait::async_trait;
use sea_orm::{DatabaseConnection, DbErr};

// Трейты для бизнес-логики
#[async_trait]
trait UserService {
    async fn register_user(&self, email: String, name: String) -> Result<UserDto, AppError>;
    async fn get_user_profile(&self, user_id: i32) -> Result<UserProfileDto, AppError>;
    // ... другие методы
}

// Реализация через репозиторий
struct UserServiceImpl<R: UserRepository> {
    repository: R,
}

#[async_trait]
impl<R: UserRepository + Send + Sync> UserService for UserServiceImpl<R> {
    async fn register_user(&self, email: String, name: String) -> Result<UserDto, AppError> {
        // Проверки и бизнес-логика
        if email.is_empty() || name.is_empty() {
            return Err(AppError::ValidationError("Email и имя обязательны".to_string()));
        }
        
        let user = self.repository.create(email, name)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        
        Ok(UserDto::from(user))
    }
    
    // ... реализация других методов
}

// Связывание в main.rs
fn main() {
    // ...
    
    let db = establish_connection().await.unwrap();
    let user_repo = SeaOrmUserRepository::new(db.clone());
    let user_service = Arc::new(UserServiceImpl { repository: user_repo });
    
    let app = Router::new()
        // ...
        .with_state(user_service);
}

3. Использование DTOs для разделения слоев

rust
use serde::{Deserialize, Serialize};
use entity::user;

// DTO для создания пользователя
#[derive(Deserialize)]
struct CreateUserDto {
    email: String,
    name: String,
    password: String,
}

// DTO для ответов API
#[derive(Serialize)]
struct UserDto {
    id: i32,
    email: String,
    name: String,
}

// Преобразование из модели в DTO
impl From<user::Model> for UserDto {
    fn from(model: user::Model) -> Self {
        Self {
            id: model.id,
            email: model.email,
            name: model.name,
        }
    }
}

4. Эффективное использование запросов

rust
use sea_orm::{EntityTrait, QuerySelect, QueryOrder, QueryFilter, ColumnTrait, Condition};
use entity::user;

// Пагинация
async fn get_paginated_users(
    db: &DatabaseConnection,
    page: u64,
    items_per_page: u64,
) -> Result<(Vec<user::Model>, u64), DbErr> {
    // Получение общего числа пользователей
    let total = user::Entity::find()
        .count(db)
        .await?;
    
    // Получение страницы с пользователями
    let users = user::Entity::find()
        .order_by_asc(user::Column::Id)
        .offset((page - 1) * items_per_page)
        .limit(items_per_page)
        .all(db)
        .await?;
    
    Ok((users, total))
}

// Сложные фильтры
async fn search_users(
    db: &DatabaseConnection,
    search_term: Option<&str>,
    active_only: bool,
) -> Result<Vec<user::Model>, DbErr> {
    let mut condition = Condition::all();
    
    // Добавляем условия поиска при наличии
    if let Some(term) = search_term {
        condition = condition
            .add(user::Column::Name.contains(term))
            .or(user::Column::Email.contains(term));
    }
    
    // Добавляем фильтр по активности, если нужно
    if active_only {
        condition = condition.add(user::Column::IsActive.eq(true));
    }
    
    // Выполняем запрос с условиями
    user::Entity::find()
        .filter(condition)
        .all(db)
        .await
}

5. Обработка ошибок

rust
use axum::{
    response::{IntoResponse, Response},
    http::StatusCode,
    Json,
};
use serde_json::json;
use sea_orm::DbErr;
use thiserror::Error;

// Определение типов ошибок приложения
#[derive(Error, Debug)]
enum AppError {
    #[error("Ошибка базы данных: {0}")]
    Database(String),
    
    #[error("Объект не найден: {0}")]
    NotFound(String),
    
    #[error("Ошибка валидации: {0}")]
    ValidationError(String),
    
    #[error("Внутренняя ошибка сервера")]
    InternalError,
}

// Преобразование ошибок Sea-ORM в ошибки приложения
impl From<DbErr> for AppError {
    fn from(err: DbErr) -> Self {
        match err {
            DbErr::RecordNotFound(_) => AppError::NotFound("Запись не найдена".to_string()),
            // ... обработка других типов ошибок
            _ => AppError::Database(err.to_string()),
        }
    }
}

// Преобразование ошибок приложения в HTTP-ответы
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            AppError::Database(message) => (StatusCode::INTERNAL_SERVER_ERROR, message),
            AppError::NotFound(message) => (StatusCode::NOT_FOUND, message),
            AppError::ValidationError(message) => (StatusCode::BAD_REQUEST, message),
            AppError::InternalError => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Внутренняя ошибка сервера".to_string(),
            ),
        };
        
        let body = Json(json!({
            "error": error_message,
        }));
        
        (status, body).into_response()
    }
}

// Использование в обработчиках
async fn get_user(
    State(db): State<Arc<DatabaseConnection>>,
    Path(user_id): Path<i32>,
) -> Result<Json<UserDto>, AppError> {
    let user = user::Entity::find_by_id(user_id)
        .one(&*db)
        .await?
        .ok_or_else(|| AppError::NotFound(format!("Пользователь с ID {} не найден", user_id)))?;
    
    Ok(Json(UserDto::from(user)))
}

Sea-ORM предоставляет мощные инструменты для работы с базами данных в Axum-приложениях. Правильное использование ORM позволяет создавать чистый, производительный и поддерживаемый код, отделяя бизнес-логику от деталей работы с базой данных.