Rust та SQLx у 2026: Запити з Перевіркою під час Компіляції та Питання на Співбесідах

Повний посібник з використання SQLx у Rust - перевірка запитів під час компіляції, порівняння з ORM та практичні питання для технічних співбесід.

Ілюстрація Rust та SQLx з перевіркою під час компіляції

Бібліотека SQLx революціонізувала спосіб, яким розробники Rust взаємодіють з базами даних. На відміну від традиційних ORM, SQLx пропонує унікальний підхід: сирі SQL-запити з перевіркою під час компіляції. Ця комбінація забезпечує типобезпеку без жертвування продуктивністю або контролем над згенерованим SQL.

SQLx 0.8+ підтримує PostgreSQL, MySQL, MariaDB та SQLite з повною асинхронною підтримкою через tokio або async-std. Перевірка запитів під час компіляції виявляє синтаксичні помилки SQL та невідповідності типів до запуску застосунку.

Чому SQLx Замість Традиційних ORM

Екосистема Rust пропонує кілька варіантів доступу до баз даних: Diesel як повнофункціональний ORM, SeaORM для підходу ActiveRecord та SQLx для сирого SQL з перевіркою типів. Кожен інструмент служить різним цілям.

Diesel генерує SQL з типобезпечних конструкторів запитів. Цей підхід добре працює для стандартних CRUD-операцій, але стає громіздким при складних запитах, що вимагають специфічних функцій бази даних. SQLx приймає іншу філософію - розробники пишуть справжні SQL-запити, які компілятор перевіряє на відповідність схемі бази даних.

rust
use sqlx::postgres::PgPoolOptions;
use sqlx::FromRow;

#[derive(Debug, FromRow)]
struct User {
    id: i32,
    email: String,
    created_at: chrono::DateTime<chrono::Utc>,
}

async fn fetch_users(pool: &sqlx::PgPool) -> Result<Vec<User>, sqlx::Error> {
    sqlx::query_as::<_, User>(
        "SELECT id, email, created_at FROM users WHERE active = $1"
    )
    .bind(true)
    .fetch_all(pool)
    .await
}

Макрос query_as перевіряє, що стовпці id, email та created_at існують у таблиці users і що їхні типи відповідають полям структури. Якщо схема бази даних зміниться, компіляція завершиться невдачею з чіткими повідомленнями про помилки.

Налаштування Перевірки під час Компіляції

Перевірка запитів під час компіляції вимагає з'єднання з базою даних під час процесу збірки. SQLx керує цим через змінну середовища DATABASE_URL або файли .env.

bash
# .env
DATABASE_URL=postgres://user:password@localhost/myapp_dev

Для CI-середовищ, де з'єднання з базою даних недоступне, SQLx надає офлайн-режим. Команда cargo sqlx prepare генерує метадані запитів, які комітяться до репозиторію.

bash
cargo install sqlx-cli
cargo sqlx prepare

Ця команда створює директорію .sqlx, що містить скомпільовану інформацію про запити. Коли DATABASE_URL не встановлено, SQLx використовує ці закешовані метадані.

rust
// Примусова офлайн-перевірка навіть коли база даних доступна
#[sqlx::test]
async fn test_user_query(pool: PgPool) {
    let users = sqlx::query_as!(
        User,
        r#"SELECT id, email, created_at FROM users LIMIT 10"#
    )
    .fetch_all(&pool)
    .await
    .unwrap();
    
    assert!(users.len() <= 10);
}

Розширені Патерни Запитів

Продакшн-застосунки вимагають більше, ніж прості CRUD-операції. SQLx підтримує складні сценарії, включаючи динамічні запити, транзакції та пакетні операції.

Динамічні Запити з Опціональними Фільтрами

rust
use sqlx::QueryBuilder;

async fn search_users(
    pool: &PgPool,
    email_filter: Option<&str>,
    min_age: Option<i32>,
    limit: i64,
) -> Result<Vec<User>, sqlx::Error> {
    let mut builder = QueryBuilder::new(
        "SELECT id, email, created_at FROM users WHERE 1=1"
    );
    
    if let Some(email) = email_filter {
        builder.push(" AND email ILIKE ");
        builder.push_bind(format!("%{}%", email));
    }
    
    if let Some(age) = min_age {
        builder.push(" AND age >= ");
        builder.push_bind(age);
    }
    
    builder.push(" LIMIT ");
    builder.push_bind(limit);
    
    builder
        .build_query_as::<User>()
        .fetch_all(pool)
        .await
}

Транзакції з Обробкою Відкату

rust
async fn transfer_funds(
    pool: &PgPool,
    from_account: i32,
    to_account: i32,
    amount: rust_decimal::Decimal,
) -> Result<(), sqlx::Error> {
    let mut tx = pool.begin().await?;
    
    // Списати з вихідного рахунку
    let result = sqlx::query!(
        r#"UPDATE accounts SET balance = balance - $1 
           WHERE id = $2 AND balance >= $1"#,
        amount,
        from_account
    )
    .execute(&mut *tx)
    .await?;
    
    if result.rows_affected() == 0 {
        return Err(sqlx::Error::RowNotFound);
    }
    
    // Зарахувати на цільовий рахунок
    sqlx::query!(
        "UPDATE accounts SET balance = balance + $1 WHERE id = $2",
        amount,
        to_account
    )
    .execute(&mut *tx)
    .await?;
    
    tx.commit().await?;
    Ok(())
}

Пакетне Вставлення

rust
async fn bulk_insert_users(
    pool: &PgPool,
    users: &[(String, String)],
) -> Result<(), sqlx::Error> {
    let mut builder = QueryBuilder::new(
        "INSERT INTO users (email, name) "
    );
    
    builder.push_values(users, |mut b, (email, name)| {
        b.push_bind(email).push_bind(name);
    });
    
    builder.build().execute(pool).await?;
    Ok(())
}

Міграції та Керування Схемою

CLI SQLx надає вбудоване керування міграціями, яке безшовно інтегрується з перевіркою під час компіляції.

bash
# Створити нову міграцію
sqlx migrate add create_users_table

# Запустити очікуючі міграції
sqlx migrate run

# Відкатити останню міграцію
sqlx migrate revert

Файли міграцій використовують нумерацію часових міток для впорядкування:

sql
-- migrations/20260915120000_create_users_table.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);

Для складних застосунків міграції можуть містити як up, так і down файли:

sql
-- migrations/20260915120000_create_users_table.up.sql
CREATE TABLE users (...);

-- migrations/20260915120000_create_users_table.down.sql
DROP TABLE users;

Пул З'єднань та Продакшн-Конфігурація

Продакшн-розгортання вимагають належної конфігурації пулу з'єднань, обробки таймаутів та моніторингу.

rust
use sqlx::postgres::{PgPoolOptions, PgConnectOptions};
use std::time::Duration;

async fn create_production_pool() -> Result<PgPool, sqlx::Error> {
    let options = PgConnectOptions::new()
        .host("db.example.com")
        .database("production")
        .username("app_user")
        .password("secure_password")
        .ssl_mode(sqlx::postgres::PgSslMode::Require);
    
    PgPoolOptions::new()
        .max_connections(20)
        .min_connections(5)
        .acquire_timeout(Duration::from_secs(5))
        .idle_timeout(Duration::from_secs(600))
        .max_lifetime(Duration::from_secs(1800))
        .connect_with(options)
        .await
}

Розмір пулу з'єднань залежить від кількох факторів: кількості ядер CPU, характеристик навантаження на базу даних та лімітів ресурсів. Типова відправна точка - 2-3 з'єднання на ядро CPU з коригуванням на основі метрик.

Обробка Помилок та Стійкість

Продакшн-застосунки баз даних мають елегантно обробляти різні сценарії збоїв.

rust
use sqlx::Error as SqlxError;

#[derive(Debug, thiserror::Error)]
enum DatabaseError {
    #[error("Record not found")]
    NotFound,
    #[error("Duplicate entry: {0}")]
    Duplicate(String),
    #[error("Connection failed: {0}")]
    Connection(String),
    #[error("Query error: {0}")]
    Query(String),
}

impl From<SqlxError> for DatabaseError {
    fn from(err: SqlxError) -> Self {
        match err {
            SqlxError::RowNotFound => DatabaseError::NotFound,
            SqlxError::Database(db_err) => {
                if let Some(code) = db_err.code() {
                    if code == "23505" {
                        return DatabaseError::Duplicate(
                            db_err.message().to_string()
                        );
                    }
                }
                DatabaseError::Query(db_err.message().to_string())
            }
            SqlxError::PoolTimedOut => DatabaseError::Connection(
                "Connection pool timeout".to_string()
            ),
            _ => DatabaseError::Query(err.to_string()),
        }
    }
}

Готовий до співбесід з Rust?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Питання на Співбесідах з SQLx

Технічні співбесіди часто досліджують розуміння взаємодії з базами даних, типобезпеки та специфічних особливостей SQLx.

Питання: Як SQLx досягає верифікації запитів під час компіляції без процедурних макросів, що модифікують рантайм?

SQLx використовує процедурні макроси, які з'єднуються з базою даних під час компіляції. Макрос query! парсить SQL-рядок, надсилає його до бази даних для валідації та генерує Rust-код, що мапить типи результатів. Ця генерація коду відбувається повністю під час компіляції - рантайм виконує лише підготовлені оператори.

Питання: Які компроміси між SQLx та Diesel для продакшн-застосунків?

Diesel забезпечує ергономічніше побудування запитів та кращу підтримку композиції запитів через систему типів. SQLx пропонує прямий доступ до SQL та кращу підтримку специфічних функцій бази даних. Diesel вимагає менше шаблонного коду для стандартних CRUD-операцій, тоді як SQLx надає більший контроль для складних запитів та оптимізації продуктивності.

Питання: Як обробляти nullable значення в SQLx з належною типобезпекою?

rust
#[derive(FromRow)]
struct UserProfile {
    id: i32,
    email: String,
    bio: Option<String>,  // Nullable в базі даних
}

// Запит автоматично обробляє null
let profile = sqlx::query_as!(
    UserProfile,
    "SELECT id, email, bio FROM user_profiles WHERE id = $1",
    user_id
)
.fetch_one(pool)
.await?;

Питання: Поясніть різницю між fetch_one, fetch_optional та fetch_all у SQLx.

fetch_one очікує рівно один рядок і повертає помилку, якщо запит повертає нуль або багато рядків. fetch_optional повертає Option<T>, обробляючи випадки, коли рядок може не існувати. fetch_all повертає Vec<T> для запитів з множинними результатами. Вибір правильного методу fetch запобігає помилкам рантайму та виражає намір у системі типів.

Питання: Як реалізувати пагінацію з SQLx?

rust
async fn paginated_users(
    pool: &PgPool,
    page: i64,
    per_page: i64,
) -> Result<(Vec<User>, i64), sqlx::Error> {
    let offset = (page - 1) * per_page;
    
    let users = sqlx::query_as!(
        User,
        r#"SELECT id, email, created_at 
           FROM users 
           ORDER BY created_at DESC 
           LIMIT $1 OFFSET $2"#,
        per_page,
        offset
    )
    .fetch_all(pool)
    .await?;
    
    let total: (i64,) = sqlx::query_as(
        "SELECT COUNT(*) FROM users"
    )
    .fetch_one(pool)
    .await?;
    
    Ok((users, total.0))
}

Порівняння Продуктивності та Бенчмарки

Продуктивність SQLx конкурує з сирими драйверами баз даних, оскільки не додає накладних витрат абстракції в рантаймі. Система типів та генерація коду під час компіляції усувають витрати на рефлексію, присутні в багатьох ORM.

Для застосунків з інтенсивним читанням пули з'єднань та правильне використання підготовлених операторів мають більший вплив, ніж вибір бібліотеки. Офлайн-режим SQLx дозволяє повністю пропустити парсинг запитів у рантаймі.

rust
// Підготовлений оператор - парситься один раз, виконується багато разів
let stmt = sqlx::query!("SELECT id, email FROM users WHERE active = $1");

for _ in 0..1000 {
    let users = stmt.clone().bind(true).fetch_all(pool).await?;
}

Підсумок

SQLx представляє прагматичний підхід до доступу до баз даних у Rust. Перевірка запитів під час компіляції виявляє помилки рано без втрати контролю над SQL. Асинхронна підтримка природно інтегрується з асинхронною екосистемою Rust, а CLI-інструментарій спрощує керування міграціями.

Для команд, які комфортно працюють з SQL і хочуть типобезпеки Rust, SQLx надає найкраще з обох світів. Система типів гарантує коректність під час компіляції, тоді як сирий SQL зберігає повний доступ до можливостей бази даних.

Розуміння патернів, представлених у цьому посібнику, готує розробників як до практичного використання, так і до технічних дискусій на співбесідах щодо архітектури баз даних та специфічних виборів для Rust.

Щоденний виклик

Чи знайдеш ти помилку в Rust?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 12 вересня 2026 р.

Теги

#rust
#sqlx
#database
#postgresql
#tutorial

Поділитися

Пов'язані статті