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 позволяет обрабатывать тысячи запросов одновременно, не блокируя потоки.