Что такое 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— для PostgreSQLmysql— для MySQLsqlite— для SQLitemssql— для 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<()> Также в библиотеке
Multer для JavaScript (Node.js): полное руководство с примерами
Валидация данных в PHP с библиотекой Respect\Validation
Prometheus метрики в Swift: мониторинг iOS/macOS приложений
Dependency Injection контейнер для Kotlin: Koin vs Kodein vs Dagger
Интернационализация (i18n) в TypeScript: библиотека i18next