Интеграция с базой данных
Сохранение данных
«Интеграция с базой данных» — бесплатный урок Learn Rust Coding на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Learn Rust Coding, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Learn Rust Coding содержит 4 уроков всего.
Сохранение данных
До сих пор наш API хранил данные в памяти, и они исчезали после перезапуска. Настоящим сервисам нужна база данных. В этом уроке Вы подключите API на Axum к PostgreSQL с помощью sqlx — асинхронного набора инструментов SQL для Rust с проверкой во время компиляции.
Вы узнаете о пулах подключений, запросах, преобразовании строк в структуры и использовании пула как общего состояния.
Добавление sqlx
Добавьте sqlx с необходимыми функциями: средой выполнения, TLS и драйвером базы данных. Ниже приведена настройка Postgres с использованием Tokio.
// Cargo.toml
// [dependencies]
// sqlx = { version = "0.7", features = [
// "runtime-tokio", "tls-rustls", "postgres", "macros"
// ] }Пул подключений
Открывать новое подключение для каждого запроса медленно. Пул подключений хранит набор повторно используемых подключений. PgPoolOptions создаёт его из URL базы данных. Пул недорого клонировать (внутри он использует подсчёт ссылок), поэтому он идеально подходит в качестве общего состояния.
use sqlx::postgres::PgPoolOptions;
async fn make_pool(url: &str) -> sqlx::PgPool {
PgPoolOptions::new()
.max_connections(5)
.connect(url)
.await
.expect("failed to connect")
}Пул как состояние приложения
Передайте пул в Axum с помощью .with_state(pool). Затем обработчики принимают State(pool): State<PgPool>. Поскольку клонирование пула почти не требует затрат, каждый запрос использует одни и те же базовые подключения.
use axum::{routing::get, Router};
use sqlx::PgPool;
fn build(pool: PgPool) -> Router {
Router::new()
.route("/todos", get(list_todos))
.with_state(pool)
}Выполнение запроса
Функция sqlx::query выполняет обычный SQL. Привязывайте параметры с помощью .bind(value), чтобы избежать SQL-инъекций; Postgres использует заполнители $1, $2. Используйте .execute(&pool) для операций записи, которые не возвращают строки.
use sqlx::PgPool;
async fn insert_todo(pool: &PgPool, title: &str) -> Result<(), sqlx::Error> {
sqlx::query("INSERT INTO todos (title, done) VALUES ($1, $2)")
.bind(title)
.bind(false)
.execute(pool)
.await?;
Ok(())
}Преобразование строк в структуры
Добавьте sqlx::FromRow к своей модели, чтобы результаты запроса напрямую преобразовывались в неё. Используйте query_as::<_, Todo> вместе с fetch_all, чтобы получить Vec<Todo>, или fetch_one для одной строки.
use sqlx::{PgPool, FromRow};
#[derive(FromRow, serde::Serialize)]
struct Todo { id: i32, title: String, done: bool }
async fn all_todos(pool: &PgPool) -> Result<Vec<Todo>, sqlx::Error> {
let rows = sqlx::query_as::<_, Todo>("SELECT id, title, done FROM todos")
.fetch_all(pool)
.await?;
Ok(rows)
}Обработчик, читающий базу данных
Объедините все части: обработчик получает пул из состояния, выполняет запрос и возвращает JSON. Преобразуйте ошибки базы данных в статус 500, чтобы клиент получил корректный ответ, а не столкнулся с аварийным завершением программы.
use axum::{extract::State, Json, http::StatusCode};
use sqlx::PgPool;
async fn list_todos(
State(pool): State<PgPool>,
) -> Result<Json<Vec<Todo>>, StatusCode> {
match all_todos(&pool).await {
Ok(todos) => Ok(Json(todos)),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}Возврат вставленных строк
Postgres может вернуть только что вставленную строку с помощью RETURNING. Объедините его с query_as и fetch_one, чтобы за один обмен получить новую запись, включая сгенерированный идентификатор.
use sqlx::PgPool;
async fn create(pool: &PgPool, title: &str) -> Result<Todo, sqlx::Error> {
let todo = sqlx::query_as::<_, Todo>(
"INSERT INTO todos (title, done) VALUES ($1, false) \
RETURNING id, title, done")
.bind(title)
.fetch_one(pool)
.await?;
Ok(todo)
}Миграции
Ваша схема должна существовать до выполнения запросов. sqlx поддерживает миграции: SQL-файлы в папке migrations/, применяемые по порядку. Запускайте их при старте с помощью sqlx::migrate!(), чтобы новая база данных настраивалась автоматически.
use sqlx::PgPool;
async fn run_migrations(pool: &PgPool) {
sqlx::migrate!("./migrations")
.run(pool)
.await
.expect("migrations failed");
}
// migrations/0001_init.sql contains the CREATE TABLE statements.Транзакции
Если несколько операций записи должны выполниться вместе, объедините их в транзакцию. Начните с помощью pool.begin(), выполните запросы через дескриптор транзакции, затем вызовите commit. Если удалить транзакцию, не зафиксировав её, sqlx автоматически отменит изменения, сохранив согласованность данных.
use sqlx::PgPool;
async fn transfer(pool: &PgPool) -> Result<(), sqlx::Error> {
let mut tx = pool.begin().await?;
sqlx::query("UPDATE accounts SET balance = balance - 10 WHERE id = 1")
.execute(&mut *tx).await?;
sqlx::query("UPDATE accounts SET balance = balance + 10 WHERE id = 2")
.execute(&mut *tx).await?;
tx.commit().await?;
Ok(())
}Конфигурация и секреты
Никогда не встраивайте реквизиты базы данных непосредственно в код. Считывайте DATABASE_URL из окружения; при разработке его часто загружают из файла .env с помощью библиотеки dotenvy. В рабочей среде платформа передаёт его как переменную окружения.
use std::env;
async fn connect_from_env() -> sqlx::PgPool {
let url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
make_pool(&url).await
}Быстрая проверка
Проверьте, насколько хорошо Вы поняли интеграцию с базой данных.
Итоги
Вы интегрировали базу данных:
- Используйте пул подключений
PgPoolи передавайте его через.with_state. query/execute— для записи;query_asвместе сFromRow— для типизированного чтения.- Всегда привязывайте параметры, чтобы предотвратить SQL-инъекции.
RETURNINGполучает вставленные строки; транзакции атомарно объединяют операции записи.- Запускайте миграции при старте и считывайте
DATABASE_URLиз окружения.
Часто задаваемые вопросы
Урок «Интеграция с базой данных» бесплатный?
Да — полный текст урока «Интеграция с базой данных» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Learn Rust Coding, подпишись на CoddyKit PRO. Курс Learn Rust Coding содержит 4 уроков всего.
Чему я научусь в уроке «Интеграция с базой данных»?
Сохранение данных Ты практикуешь Learn Rust Coding с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Learn Rust Coding?
Предыдущий опыт не требуется. Learn Rust Coding на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Интеграция с базой данных»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Learn Rust Coding?
Да. Каждый урок Learn Rust Coding включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Настройка проекта
- Конечные точки и модели
- Интеграция с базой данных
- Тестирование API