SQLx для Rust: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое SQLx и зачем он нужен?

SQLx — это асинхронная, чистая Rust-библиотека для работы с базами данных, которая реализует подход "SQL без ORM". В отличие от таких ORM-решений, как Diesel или SeaORM, SQLx не скрывает SQL-запросы за абстракциями, а позволяет писать чистый SQL-код, при этом обеспечивая полную безопасность типов на этапе компиляции.

Основные преимущества SQLx:

  • Безопасность типов во время компиляции — SQLx проверяет ваши SQL-запросы на соответствие схеме базы данных прямо во время сборки проекта.
  • Асинхронность из коробки — библиотека полностью поддерживает async/await, что идеально подходит для высоконагруженных веб-приложений.
  • Поддержка множества СУБД — PostgreSQL, MySQL, SQLite и MSSQL.
  • Отсутствие магии — вы пишете обычный SQL, а библиотека только помогает безопасно передавать параметры и получать результаты.
  • Миграции — встроенная система управления миграциями схемы базы данных.

SQLx идеально подходит для проектов, где важен полный контроль над запросами, производительность и безопасность типов, но при этом не хочется изучать сложные DSL (предметно-ориентированные языки) ORM.

Установка

SQLx устанавливается через менеджер пакетов Cargo. Добавьте зависимость в файл Cargo.toml:

[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"

В зависимости от используемой базы данных выберите соответствующий feature-флаг:

  • postgres — для PostgreSQL
  • mysql — для MySQL
  • sqlite — для SQLite
  • mssql — для Microsoft SQL Server

Для runtime вы можете выбрать runtime-tokio-rustls (рекомендуется) или runtime-async-std-rustls.

Быстрый старт

Создадим простое приложение, которое подключается к PostgreSQL и выполняет запрос:

use sqlx::postgres::PgPoolOptions;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    // Создаем пул соединений
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect("postgres://user:password@localhost/mydb")
        .await?;

    // Выполняем простой запрос
    let row: (i64,) = sqlx::query_as("SELECT $1::bigint")
        .bind(42_i64)
        .fetch_one(&pool)
        .await?;

    println!("Результат: {}", row.0);

    Ok(())
}

Этот код создает пул из 5 соединений к базе данных, выполняет запрос с параметром и выводит результат.

Основные методы и функции SQLx

1. sqlx::query

Что делает: Создает новый SQL-запрос с возможностью привязки параметров. Это базовый строительный блок для всех операций.

Параметры:

  • sql: &str — строка SQL-запроса с плейсхолдерами ($1, $2 для PostgreSQL; ? для MySQL/SQLite)

Пример:

use sqlx::query;

let query = sqlx::query("SELECT * FROM users WHERE id = $1 AND active = $2")
    .bind(1)
    .bind(true);

2. .bind()

Что делает: Привязывает значение к параметру запроса. Порядок вызовов соответствует порядку плейсхолдеров в SQL.

Параметры:

  • value: impl Encode + Type<DB> — любое значение, которое можно закодировать для БД (i32, String, bool, Option и т.д.)

Пример:

let user_id: i32 = 42;
let user_name: &str = "Alice";

sqlx::query("INSERT INTO users (id, name) VALUES ($1, $2)")
    .bind(user_id)
    .bind(user_name)
    .execute(&pool)
    .await?;

3. .fetch_one()

Что делает: Выполняет запрос и возвращает ровно одну строку. Если строк больше или меньше — паникует или возвращает ошибку.

Параметры:

  • executor: impl Executor<'_> — пул соединений или транзакция

Пример:

let user: (i32, String, bool) = sqlx::query_as(
    "SELECT id, name, active FROM users WHERE id = $1"
)
.bind(1)
.fetch_one(&pool)
.await?;

println!("ID: {}, Name: {}, Active: {}", user.0, user.1, user.2);

4. .fetch_optional()

Что делает: Выполняет запрос и возвращает Option с одной строкой. Если строк нет — возвращает None.

Параметры: Аналогично fetch_one.

Пример:

let user: Option<(i32, String)> = sqlx::query_as(
    "SELECT id, name FROM users WHERE id = $1"
)
.bind(999)
.fetch_optional(&pool)
.await?;

match user {
    Some((id, name)) => println!("Найден: {} - {}", id, name),
    None => println!("Пользователь не найден"),
}

5. .fetch_all()

Что делает: Выполняет запрос и возвращает все строки в виде вектора.

Параметры: Аналогично fetch_one.

Пример:

let users: Vec<(i32, String)> = sqlx::query_as(
    "SELECT id, name FROM users WHERE active = $1"
)
.bind(true)
.fetch_all(&pool)
.await?;

for (id, name) in users {
    println!("{}: {}", id, name);
}

6. .execute()

Что делает: Выполняет запрос, который не возвращает строк (INSERT, UPDATE, DELETE). Возвращает количество затронутых строк.

Параметры: Аналогично fetch_one.

Пример:

let result = sqlx::query("UPDATE users SET active = $1 WHERE id = $2")
    .bind(false)
    .bind(1)
    .execute(&pool)
    .await?;

println!("Обновлено строк: {}", result.rows_affected());

7. sqlx::query_as()

Что делает: Создает запрос, который автоматически преобразует строки в пользовательскую структуру, реализующую трейт FromRow.

Параметры:

  • sql: &str — SQL-запрос

Пример:

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

let users: Vec<User> = sqlx::query_as::<_, User>(
    "SELECT id, name, email FROM users WHERE active = $1"
)
.bind(true)
.fetch_all(&pool)
.await?;

for user in users {
    println!("{}: {} ({})", user.id, user.name, user.email);
}

8. sqlx::query_scalar()

Что делает: Создает запрос, который возвращает одно значение (первую колонку первой строки). Удобно для агрегатных функций.

Параметры:

  • sql: &str — SQL-запрос

Пример:

let count: i64 = sqlx::query_scalar(
    "SELECT COUNT(*) FROM users WHERE active = $1"
)
.bind(true)
.fetch_one(&pool)
.await?;

println!("Активных пользователей: {}", count);

9. .begin() и транзакции

Что делает: Начинает транзакцию. Позволяет выполнять несколько запросов атомарно.

Параметры: Нет (метод пула).

Пример:

let mut tx = pool.begin().await?;

sqlx::query("UPDATE accounts SET balance = balance - 100 WHERE id = $1")
    .bind(1)
    .execute(&mut *tx)
    .await?;

sqlx::query("UPDATE accounts SET balance = balance + 100 WHERE id = $2")
    .bind(2)
    .execute(&mut *tx)
    .await?;

tx.commit().await?;
println!("Транзакция успешно завершена");

10. sqlx::migrate!()

Что делает: Выполняет миграции базы данных из указанной директории. Миграции — это SQL-файлы с версионированием.

Параметры: Путь к директории с миграциями (макрос).

Пример:

// В Cargo.toml нужно добавить:
// sqlx = { version = "0.7", features = ["migrate"] }

use sqlx::migrate::Migrator;
use std::path::Path;

let migrator = Migrator::new(Path::new("./migrations")).await?;
migrator.run(&pool).await?;

// Структура директории migrations:
// migrations/
//   20240101_initial.sql
//   20240102_add_email.sql

Реальный практический пример

Создадим REST API для управления задачами (todo list) с использованием Actix-web и SQLx:

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

#[derive(Debug, Serialize, Deserialize, FromRow)]
struct Task {
    id: i32,
    title: String,
    completed: bool,
}

#[derive(Debug, Deserialize)]
struct CreateTaskRequest {
    title: String,
}

async fn get_tasks(pool: web::Data<sqlx::PgPool>) -> impl Responder {
    let tasks = sqlx::query_as::<_, Task>(
        "SELECT id, title, completed FROM tasks ORDER BY id"
    )
    .fetch_all(pool.get_ref())
    .await;

    match tasks {
        Ok(tasks) => HttpResponse::Ok().json(tasks),
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

async fn create_task(
    pool: web::Data<sqlx::PgPool>,
    body: web::Json<CreateTaskRequest>,
) -> impl Responder {
    let result = sqlx::query(
        "INSERT INTO tasks (title, completed) VALUES ($1, false) RETURNING id, title, completed"
    )
    .bind(&body.title)
    .fetch_one(pool.get_ref())
    .await;

    match result {
        Ok(row) => {
            let task = Task {
                id: row.get(0),
                title: row.get(1),
                completed: row.get(2),
            };
            HttpResponse::Created().json(task)
        }
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

async fn toggle_task(
    pool: web::Data<sqlx::PgPool>,
    path: web::Path<i32>,
) -> impl Responder {
    let task_id = path.into_inner();

    let result = sqlx::query(
        "UPDATE tasks SET completed = NOT completed WHERE id = $1 RETURNING id, title, completed"
    )
    .bind(task_id)
    .fetch_one(pool.get_ref())
    .await;

    match result {
        Ok(row) => {
            let task = Task {
                id: row.get(0),
                title: row.get(1),
                completed: row.get(2),
            };
            HttpResponse::Ok().json(task)
        }
        Err(e) => HttpResponse::NotFound().body(e.to_string()),
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()>                    

Рекомендаций