データベース統合
データを永続化します
「データベース統合」は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フィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- プロジェクトのセットアップ
- エンドポイントとモデル
- データベース統合
- APIのテスト