Базы данных (sea-orm) в Axum
Интеграция Axum с базами данных является важной частью разработки веб-приложений. В этом разделе мы рассмотрим, как использовать Sea-ORM — современный асинхронный ORM для Rust — в приложениях на Axum.
Содержание
- Подключение Sea-ORM к проекту
- Настройка подключения к базе данных
- Определение моделей данных
- Базовые операции CRUD
- Интеграция с Axum
- Миграции
- Транзакции
- Лучшие практики
Подключение Sea-ORM к проекту
Для начала добавьте необходимые зависимости в Cargo.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",
] }
Настройка подключения к базе данных
Создание и настройка соединения с базой данных:
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 предоставляет инструмент командной строки для генерации кода моделей из существующей базы данных:
# Установка sea-orm-cli
cargo install sea-orm-cli
# Генерация сущностей из существующей базы данных
sea-orm-cli generate entity \
-u postgres://username:password@localhost/database \
-o src/entity
Ручное определение Entity
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 (Создание)
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 (Чтение)
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 (Обновление)
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 (Удаление)
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 в обработчиках
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))
}
Создание репозитория для разделения бизнес-логики
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
:
[dependencies]
# ... другие зависимости
sea-orm-migration = "0.12.3"
Создание миграций
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,
}
Запуск миграций
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 поддерживает транзакции для атомарного выполнения нескольких операций:
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. Создание пула соединений
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. Абстракция бизнес-логики через репозитории
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 для разделения слоев
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. Эффективное использование запросов
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. Обработка ошибок
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 позволяет создавать чистый, производительный и поддерживаемый код, отделяя бизнес-логику от деталей работы с базой данных.