ActixMiddleTechnical

Как Actix-web интегрируется с асинхронными драйверами баз данных, такими как sqlx?

Создайте PgPool через PgPoolOptions::new().connect() и передайте его в App как web::Data. В хендлерах принимайте pool: web::Data<sqlx::PgPool> и используйте sqlx::query_as! или query! для асинхронных запросов.

Интеграция Actix-web с sqlx

Actix-web не имеет встроенной ORM — для работы с PostgreSQL, MySQL или SQLite используется sqlx, асинхронная библиотека с проверкой запросов на этапе компиляции. Пул соединений передаётся через механизм web::Data.

Зависимости (Cargo.toml)

// Cargo.toml
[dependencies]
actix-web = "4"
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls", "uuid", "chrono", "macros"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
uuid = { version = "1", features = ["serde", "v4"] }

Создание пула и передача в приложение

use actix_web::{web, App, HttpServer, HttpResponse, Error};
use sqlx::postgres::PgPoolOptions;
use serde::Serialize;

#[derive(Serialize, sqlx::FromRow)]
struct User {
    id: i32,
    name: String,
    email: String,
}

async fn list_users(pool: web::Data<sqlx::PgPool>) -> Result<HttpResponse, Error> {
    let users = sqlx::query_as::<_, User>(
        "SELECT id, name, email FROM users ORDER BY id"
    )
    .fetch_all(pool.get_ref())
    .await
    .map_err(actix_web::error::ErrorInternalServerError)?;

    Ok(HttpResponse::Ok().json(users))
}

#[actix_web::main]
async fn main() -> std::io::Result<> {
    let database_url = std::env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");

    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect(&database_url)
        .await
        .expect("Failed to create pool");

    // Применяем миграции из папки ./migrations
    sqlx::migrate!("./migrations")
        .run(&pool)
        .await
        .expect("Migration failed");

    let pool_data = web::Data::new(pool);

    HttpServer::new(move || {
        App::new()
            .app_data(pool_data.clone())
            .route("/users", web::get().to(list_users))
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

Использование compile-time query! макроса

Макрос sqlx::query! проверяет SQL на этапе компиляции, если установлена переменная окружения DATABASE_URL или используется sqlx prepare:

async fn get_user(
    pool: web::Data<sqlx::PgPool>,
    path: web::Path<i32>,
) -> Result<HttpResponse, Error> {
    let user_id = path.into_inner();
    let rec = sqlx::query!(
        "SELECT id, name, email FROM users WHERE id = $1",
        user_id
    )
    .fetch_optional(pool.get_ref())
    .await
    .map_err(actix_web::error::ErrorInternalServerError)?;

    match rec {
        Some(row) => Ok(HttpResponse::Ok().json(serde_json::json!({
            "id": row.id,
            "name": row.name,
            "email": row.email,
        }))),
        None => Ok(HttpResponse::NotFound().finish()),
    }
}

Транзакции

async fn transfer_funds(
    pool: web::Data<sqlx::PgPool>,
    body: web::Json<Transfer>,
) -> Result<HttpResponse, Error> {
    let mut tx = pool.begin().await
        .map_err(actix_web::error::ErrorInternalServerError)?;

    sqlx::query!("UPDATE accounts SET balance = balance - $1 WHERE id = $2",
        body.amount, body.from_id)
        .execute(&mut *tx).await
        .map_err(actix_web::error::ErrorInternalServerError)?;

    sqlx::query!("UPDATE accounts SET balance = balance + $1 WHERE id = $2",
        body.amount, body.to_id)
        .execute(&mut *tx).await
        .map_err(actix_web::error::ErrorInternalServerError)?;

    tx.commit().await
        .map_err(actix_web::error::ErrorInternalServerError)?;

    Ok(HttpResponse::Ok().finish())
}

Подводные камни

  • Неправильный размер пула — по умолчанию PgPool создаёт до 10 соединений; под нагрузкой запросы будут ждать свободного слота. Устанавливайте max_connections в соответствии с лимитом БД.
  • pool.get_ref() vs &**pool — оба варианта корректны, но смешение может запутать читателей; придерживайтесь одного стиля в проекте.
  • DATABASE_URL на этапе компиляцииsqlx::query! требует живую БД или кэш sqlx-data.json (создаётся через cargo sqlx prepare) в CI.
  • Незакрытые транзакции — если обработчик вернул ошибку до tx.commit(), транзакция откатывается при drop, но только если не игнорируются ошибки между запросами.
  • N+1 запросы — sqlx не предоставляет lazy loading; при вложенных данных легко написать цикл запросов. Используйте JOIN или fetch_all с ручной сборкой.
  • Хаотичная обработка ошибокsqlx::Error содержит много вариантов; оборачивая через ErrorInternalServerError, вы теряете детали. Лучше использовать кастомный тип ошибки с ResponseError.
  • Миграции в productionsqlx::migrate!() в main() удобен для разработки, но в production может вызвать проблемы при горизонтальном масштабировании — запускайте миграции отдельным шагом деплоя.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics