0Pricing
Learn Rust Coding · レッスン

データベース統合

データを永続化します

「データベース統合」はCoddyKit上の無料Learn Rust Codingレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLearn Rust Coding学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Learn Rust Codingコースには全4レッスンが含まれています。

データを永続化する

これまでAPIはデータをメモリに保存していたため、再起動するとデータが消えていました。実際のサービスにはデータベースが必要です。このレッスンでは、Rust向けの非同期かつコンパイル時チェックに対応したSQLツールキットsqlxを使い、Axum APIをPostgreSQLに接続します。

接続プール、クエリ、行から構造体へのマッピング、共有状態としてのプールの使用方法を学びます。

sqlxを追加する

必要な機能(ランタイム、TLS、データベースドライバー)を指定してsqlxを追加します。以下はTokioを使ったPostgresの設定です。

// 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")
}

アプリケーション状態としてのプール

.with_state(pool)でプールをAxumに渡します。するとハンドラーでは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を実行します。SQLインジェクションを防ぐには.bind(value)でパラメーターをバインドします。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を導出すると、クエリ結果を直接マッピングできます。fetch_allとともにquery_as::<_, Todo>を使うとVec<Todo>を取得でき、1行だけ取得する場合は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を組み合わせると、生成されたIDを含む新しいレコードを1回のラウンドトリップで取得できます。

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はマイグレーションに対応しており、migrations/フォルダー内のSQLファイルを順番に適用します。起動時に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を環境から読み取ります。開発中は、dotenvyクレートを使って.envファイルから読み込むことがよくあります。本番環境では、プラットフォームが環境変数として注入します。

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を使用し、型付きの読み取りにはFromRowとquery_asを使用します。
  • SQLインジェクションを防ぐため、必ずパラメーターをバインドします。
  • RETURNINGで挿入した行を取得し、トランザクションで書き込みをアトミックにまとめます。
  • 起動時にマイグレーションを実行し、環境変数からDATABASE_URLを読み取ります。

よくある質問

「データベース統合」レッスンは無料ですか?

はい。「データベース統合」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Learn Rust Codingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Learn Rust Codingコースには全4レッスンが含まれています。

「データベース統合」で何を学びますか?

データを永続化します ブラウザで直接実行するハンズオンコードでLearn Rust Codingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Learn Rust Codingを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのLearn Rust Codingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「データベース統合」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このLearn Rust Codingレッスンでコードを書いて実行できますか?

はい。すべてのLearn Rust Codingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. プロジェクトのセットアップ
  2. エンドポイントとモデル
  3. データベース統合
  4. APIのテスト
← Learn Rust Codingに戻る