Работа с базами данных в Rust: PostgreSQL и MongoDB на практике

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

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

Начать курс

Введение: Почему Rust и базы данных — это мощная комбинация

Rust — это системный язык программирования, который сочетает в себе производительность C/C++ с безопасностью памяти. Однако долгое время экосистема работы с базами данных в Rust считалась "сырой". Сегодня ситуация кардинально изменилась: библиотеки sqlx (для PostgreSQL) и официальный драйвер mongodb предоставляют зрелые, асинхронные и безопасные API.

В этой статье мы на практике разберем, как подключаться к PostgreSQL и MongoDB из Rust, выполнять CRUD-операции и обрабатывать ошибки. Все примеры будут использовать асинхронный рантайм tokio.

1. Настройка проекта и установка зависимостей

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

cargo new rust_db_example
cd rust_db_example

Добавьте в Cargo.toml следующие зависимости:

[dependencies]
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres"] }
mongodb = "2.8"
serde = { version = "1", features = ["derive"] }
anyhow = "1"
  • sqlx — асинхронный драйвер для PostgreSQL с проверкой запросов на этапе компиляции.
  • mongodb — официальный драйвер от MongoDB Inc.
  • serde — для сериализации/десериализации структур.
  • anyhow — для удобной обработки ошибок.

2. Работа с PostgreSQL через sqlx

2.1 Подключение к базе данных

Для подключения используйте строку подключения (connection string) вида postgres://user:password@localhost/dbname. Пример:

use sqlx::postgres::PgPool;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let pool = PgPool::connect("postgres://postgres:password@localhost/testdb").await?;
    println!("Подключение к PostgreSQL установлено!");
    Ok(())
}

2.2 Создание таблицы и вставка данных

Определим структуру User и выполним миграцию:

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

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

async fn create_table(pool: &PgPool) -> Result<()> {
    sqlx::query(
        "CREATE TABLE IF NOT EXISTS users (
            id SERIAL PRIMARY KEY,
            name VARCHAR(255) NOT NULL,
            email VARCHAR(255) NOT NULL UNIQUE
        )"
    )
    .execute(pool)
    .await?;
    println!("Таблица users создана");
    Ok(())
}

async fn insert_user(pool: &PgPool, name: &str, email: &str) -> Result<User> {
    let user = sqlx::query_as::<_, User>(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email"
    )
    .bind(name)
    .bind(email)
    .fetch_one(pool)
    .await?;
    Ok(user)
}

Обратите внимание на $1, $2 — это параметризованные запросы, защищающие от SQL-инъекций.

2.3 Чтение и обновление данных

async fn get_user_by_email(pool: &PgPool, email: &str) -> Result<Option<User>> {
    let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE email = $1")
        .bind(email)
        .fetch_optional(pool)
        .await?;
    Ok(user)
}

async fn update_user_name(pool: &PgPool, id: i32, new_name: &str) -> Result<()> {
    sqlx::query("UPDATE users SET name = $1 WHERE id = $2")
        .bind(new_name)
        .bind(id)
        .execute(pool)
        .await?;
    println!("Пользователь {} обновлен", id);
    Ok(())
}

Использование fetch_optional позволяет обработать случай, когда запись не найдена, без паники.

3. Работа с MongoDB через официальный драйвер

3.1 Подключение и выбор базы данных

MongoDB использует документную модель. В Rust это отображается на структуры с #[derive(Serialize, Deserialize)].

use mongodb::{Client, Collection};
use mongodb::bson::{doc, Document};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Product {
    #[serde(rename = "_id")]
    id: Option<String>,
    name: String,
    price: f64,
    in_stock: bool,
}

async fn connect_mongo() -> Result<Collection<Product>> {
    let client = Client::with_uri_str("mongodb://localhost:27017").await?;
    let db = client.database("shop");
    let collection: Collection<Product> = db.collection("products");
    println!("Подключение к MongoDB установлено");
    Ok(collection)
}

3.2 CRUD-операции с MongoDB

Вставка:

async fn insert_product(collection: &Collection<Product>, product: &Product) -> Result<()> {
    let result = collection.insert_one(product).await?;
    println!("Вставлен документ с id: {:?}", result.inserted_id);
    Ok(())
}

Поиск и обновление:

use mongodb::bson::oid::ObjectId;

async fn find_product_by_name(collection: &Collection<Product>, name: &str) -> Result<Option<Product>> {
    let filter = doc! { "name": name };
    let product = collection.find_one(filter).await?;
    Ok(product)
}

async fn update_product_price(collection: &Collection<Product>, name: &str, new_price: f64) -> Result<()> {
    let filter = doc! { "name": name };
    let update = doc! { "$set": { "price": new_price } };
    collection.update_one(filter, update).await?;
    println!("Цена товара '{}' обновлена", name);
    Ok(())
}

Удаление:

async fn delete_product(collection: &Collection<Product>, name: &str) -> Result<()> {
    let filter = doc! { "name": name };
    collection.delete_one(filter).await?;
    println!("Товар '{}' удален", name);
    Ok(())
}

Блоги

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