Rust и PostgreSQL: пишем производительные запросы без боли

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

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

Начать курс

Rust и PostgreSQL: дуэт, который делает ваш код быстрее

Вы когда-нибудь задумывались, почему всё больше backend-разработчиков переходят на Rust? Ответ прост: Rust даёт контроль над памятью и скоростью, а PostgreSQL — над данными. Вместе они образуют связку, которая способна выдержать нагрузку сотен тысяч запросов в секунду без "тормозов" и утечек.

В этом руководстве мы разберём, как подружить Rust и PostgreSQL. Вы узнаете, как настроить подключение, выполнять запросы, избегать SQL-инъекций и строить асинхронные приложения. Поехали!

Почему именно Rust и PostgreSQL?

Rust — это не просто модный язык. Его система владения (ownership) и borrow checker гарантируют, что вы не получите null pointer или висячие ссылки. А PostgreSQL — это не просто БД. Это мощный инструмент с поддержкой JSON, полнотекстового поиска и сложных транзакций.

Когда вы соединяете Rust и PostgreSQL, вы получаете:

  • Безопасность — Rust не даст вам случайно отправить некорректный SQL.
  • Производительность — асинхронные драйверы работают на уровне C.
  • Контроль — вы сами решаете, когда и как пулить данные.

Установка и настройка окружения

Прежде чем писать код, убедитесь, что у вас установлены:

  • Rust (последняя стабильная версия) — rustup update
  • PostgreSQL (локально или через Docker) — docker run --name pg -e POSTGRES_PASSWORD=pass -p 5432:5432 -d postgres
  • Библиотека libpq-dev (для Linux) или её аналог на Mac/Windows

Создайте новый проект:

cargo new rust-postgres-guide
cd rust-postgres-guide

Выбор драйвера: tokio-postgres vs sqlx

Есть два основных подхода к работе с PostgreSQL в Rust:

  • tokio-postgres — низкоуровневый асинхронный драйвер. Даёт полный контроль.
  • sqlx — высокоуровневый асинхронный драйвер с проверкой запросов на этапе компиляции.

Для продвинутых проектов я рекомендую sqlx — он удобнее и безопаснее. Но для максимального контроля используйте tokio-postgres.

Добавим зависимости в Cargo.toml:

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

Первое подключение: Rust и PostgreSQL встречаются

Напишем простой пример, который подключается к базе и возвращает версию PostgreSQL.

use sqlx::postgres::PgPoolOptions;
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Строка подключения: postgres://user:password@host:port/db
    let database_url = "postgres://postgres:pass@localhost:5432/postgres";
    
    // Создаём пул соединений (reusable connections)
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(database_url)
        .await?;

    // Выполняем запрос
    let row: (String,) = sqlx::query_as("SELECT version()")
        .fetch_one(&pool)
        .await?;

    println!("PostgreSQL version: {}", row.0);
    Ok(())
}

Запустите код: cargo run. Если всё настроено правильно, вы увидите версию PostgreSQL. Обратите внимание на PgPoolOptions — пул соединений обязателен для production. Он переиспользует соединения и не создаёт их заново под каждый запрос.

Создание таблиц и CRUD-операции

Теперь давайте создадим таблицу users и выполним базовые операции: вставка, чтение, обновление, удаление.

Миграция: создаём таблицу

В sqlx миграции — это отдельные SQL-файлы. Создайте папку migrations и файл 20240101_initial.sql:

CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Запустите миграцию в коде:

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

// В main() перед запросами:
let migrator = Migrator::new(Path::new("./migrations")).await?;
migrator.run(&pool).await?;
println!("Миграции выполнены!");

Вставка данных (INSERT)

use serde::{Deserialize, Serialize};
use sqlx::FromRow;

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

// Вставляем пользователя
let new_user = sqlx::query_as::<_, User>(
    "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
)
.bind("Alice")
.bind("alice@example.com")
.fetch_one(&pool)
.await?;

println!("Создан пользователь: {:?}", new_user);

Обратите внимание на $1, $2 — это параметризованные запросы. Они защищают от SQL-инъекций. Никогда не используйте конкатенацию строк для SQL!

Чтение данных (SELECT)

// Все пользователи
let users: Vec<User> = sqlx::query_as("SELECT * FROM users")
    .fetch_all(&pool)
    .await?;

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

// Один пользователь по email
let user: Option<User> = sqlx::query_as(
    "SELECT * FROM users WHERE email = $1"
)
.bind("alice@example.com")
.fetch_optional(&pool)
.await?;

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

Обновление и удаление

// Обновление
let updated = sqlx::query(
    "UPDATE users SET name = $1 WHERE email = $2"
)
.bind("Alice Smith")
.bind("alice@example.com")
.execute(&pool)
.await?;

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

// Удаление
let deleted = sqlx::query("DELETE FROM users WHERE email = $1")
    .bind("alice@example.com")
    .execute(&pool)
    .await?;

println!("Удалено строк: {}", deleted.rows_affected());

Асинхронность и производительность: Rust в своей стихии

Одна из главных фишек Rust — асинхронность. Связка Rust и PostgreSQL через tokio позволяет обрабатывать тысячи запросов одновременно, не блокируя потоки.

Блоги

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