Contents
開発環境の準備
Axum とデータベースを組み合わせて安全に API を作る第一歩は、ローカルの Rust ツールチェーンと自動リビルドツールを整えることです。ここでは stable の Rustup インストール手順と、コード変更時に即座に再コンパイル・再起動してくれる cargo-watch のセットアップ方法を解説します。これだけで開発サイクルが大幅に短縮され、本題の DB 接続実装へスムーズに移行できます。
Rust と Cargo のインストール
- 公式サイト https://rustup.rs からインストーラを取得し、シェルで実行します。
bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - インストールが完了したらパスを更新し、ツールチェーンのバージョンを確認します(stable がインストールされていることをチェックしてください)。
bash
rustc --version # 例: rustc 1.78.0 (2024‑10‑01)
cargo --version # 例: cargo 1.78.0 (2024‑10‑01) - 必要に応じてコード品質向上のためのコンポーネントを追加します。
bash
rustup component add clippy rustfmt
cargo-watch の導入
cargo-watch はファイルシステムの変更を監視し、指定した Cargo コマンドを自動実行してくれる便利ツールです。インストールは次の一行で完了します。
|
1 2 |
cargo install cargo-watch |
基本的な使い方
プロジェクトディレクトリで以下を実行すると、ソースが保存されるたびに cargo run が走ります。
|
1 2 |
cargo watch -x "run" |
このコマンドはローカル開発だけでなく、Docker コンテナ内でも同様に利用できるため、後述するコンテナ化開発フローでも活躍します。
プロジェクト構成とクレート設定
本章では Axum アプリの雛形作成から、必須依存クレートと環境変数管理までを体系的に示します。正しい Cargo.toml と .env の設定は、マイグレーションや接続プール構築の土台となり、ビルドエラーや実行時トラブルを未然に防ぎます。
Cargo.toml に記述すべきクレート
以下は 執筆時点で安定版 の主要クレートを示した例です。バージョン指定は ^(キャレット)で「現在の最新メジャーバージョン以上」を表し、将来的に更新があっても Cargo が自動的に適合できるようにしています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
[package] name = "axum-db-sample" version = "0.1.0" edition = "2021" [dependencies] # Web フレームワークと非同期ランタイム axum = { version = "^0.7", features = ["json"] } tokio = { version = "^1", features = ["full"] } # 環境変数・シリアライズ周り dotenvy = "^0.15" serde = { version = "^1", features = ["derive"] } serde_json = "^1" # SQL クエリの型安全化(runtime は tokio + rustls) sqlx = { version = "^0.7", features = [ "runtime-tokio-rustls", "postgres", "mysql", "sqlite", "macros" ] } # フル機能 ORM(SeaORM)とマイグレーション sea-orm = { version = "^1", features = ["sqlx-postgres","sqlx-mysql","sqlx-sqlite"] } sea-orm-migration = "^1" # エラーハンドリング・ミドルウェア支援 thiserror = "^1" tower = "^0.4" |
ポイント
-axumとtokioは非同期 Web アプリの基盤です。
-sqlxのmacros機能を有効にすると、コンパイル時にクエリが検証できるのでバグが減ります。
-sea-orm系は「ORM が必要」なケース用に同梱していますが、どちらか一方だけでも問題ありません。
.env の作り方と dotenvy の利用
プロジェクトルートに .env を配置し、データベース接続文字列を記述します。.gitignore に追加してリポジトリにコミットされないようにしてください。
|
1 2 3 4 5 6 7 8 9 |
# PostgreSQL(推奨) DATABASE_URL=postgres://user:password@localhost:5432/axum_db # MySQL へ切り替える場合は下記のコメントアウトを外すだけで OK # DATABASE_URL=mysql://user:password@localhost:3306/axum_db # SQLite(ファイルベース)例 # DATABASE_URL=sqlite://./data.db |
アプリ起動時に次のコードだけを書けば、std::env::var("DATABASE_URL") が利用可能になります。
|
1 2 3 4 5 6 7 8 9 |
use dotenvy::dotenv; use std::env; fn init_env() -> Result<(), std::env::VarError> { dotenv().ok(); // .env が存在すればロード env::var("DATABASE_URL")?; // 必要なら取得だけでチェックも可能 Ok(()) } |
データベースマイグレーションと接続プール(sqlx)
データスキーマはコードと同様にバージョン管理が必要です。このセクションでは sqlx-cli によるマイグレーションの作成・適用手順、そして Axum へ非同期プールを注入する具体的な実装例を示します。
sqlx-cli のインストールとプロジェクト初期化
|
1 2 |
cargo install sqlx-cli --features mysql,postgres,sqlite |
インストール後にマイグレーション用ディレクトリを作成します。
|
1 2 |
sqlx migrate init # → migrations/ が生成される |
migrations/ 配下に .sql ファイルが追加され、バージョン番号は sqlx migrate add <description> で自動付与されます。
例:テーブル作成マイグレーション(PostgreSQL / MySQL 共通)
|
1 2 3 4 5 6 7 8 |
-- migrations/20240915120000_create_items_table.sql CREATE TABLE items ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, quantity INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); |
SQLite 用は BIGSERIAL を INTEGER PRIMARY KEY AUTOINCREMENT に置き換えるだけで動作します。
非同期接続プールの構築と Axum への組み込み
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use axum::{Router, routing::get, Extension}; use sqlx::{any::AnyPool, any::AnyPoolOptions}; /// DB プールを生成し、Extension に包んで返すユーティリティ async fn db_pool() -> Result<Extension<AnyPool>, anyhow::Error> { let database_url = std::env::var("DATABASE_URL")?; // AnyPool は PostgreSQL / MySQL / SQLite を同一インタフェースで扱える let pool = AnyPoolOptions::new() .max_connections(5) .connect(&database_url) .await?; Ok(Extension(pool)) } |
main.rs のエントリポイントでは次のようにプールを組み込みます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#[tokio::main] async fn main() -> Result<(), anyhow::Error> { // 環境変数ロード(dotenvy) dotenvy::dotenv().ok(); let db_layer = db_pool().await?; let app = Router::new() .route("/items", get(list_items)) .layer(db_layer); axum::Server::bind(&"0.0.0.0:8080".parse()?) .serve(app.into_make_service()) .await?; Ok(()) } |
これでハンドラは Extension<AnyPool> からプールを取得でき、リクエストごとに安全にコネクションを借りることが可能です。
Axum ハンドラでの CRUD 実装(sqlx)
本節では items テーブルに対する基本的な CRUD エンドポイントを実装し、型安全なクエリと統一フォーマットのエラーハンドリング手法を示します。コードは 最低限の依存関係 に抑えているため、他プロジェクトへの流用が容易です。
エンドポイント設計の概要
| メソッド | パス | 目的 |
|---|---|---|
| GET | /items |
全件取得 |
| POST | /items |
新規レコード作成 |
| PUT | /items/:id |
指定 ID の更新 |
| DELETE | /items/:id |
指定 ID の削除 |
1. 共通レスポンス型とエラー定義
以下は API 全体で使うシリアライズ可能なラッパーと、thiserror によるカスタムエラーです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
use axum::{Json, response::IntoResponse}; use serde::Serialize; use thiserror::Error; #[derive(Serialize)] struct ApiResponse<T> { data: T, } #[derive(Error, Debug)] enum ApiError { #[error("Database error")] Db(#[from] sqlx::Error), #[error("Invalid request payload")] BadRequest, } // IntoResponse 実装でエラーを JSON へ変換 impl IntoResponse for ApiError { fn into_response(self) -> axum::response::Response { let (status, msg) = match self { ApiError::Db(_) => ( axum::http::StatusCode::INTERNAL_SERVER_ERROR, "内部サーバエラー", ), ApiError::BadRequest => ( axum::http::StatusCode::BAD_REQUEST, "リクエストが不正です", ), }; (status, Json(serde_json::json!({ "error": msg }))).into_response() } } |
2. データモデル(sqlx の query_as! 用)
|
1 2 3 4 5 6 7 8 9 10 11 |
use sqlx::FromRow; use chrono::{DateTime, Utc}; #[derive(Debug, FromRow, Serialize)] struct Item { id: i64, name: String, quantity: i32, created_at: DateTime<Utc>, } |
3. ハンドラ実装例(一覧取得)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use axum::{extract::Extension, Json}; async fn list_items( Extension(pool): Extension<AnyPool>, ) -> Result<Json<ApiResponse<Vec<Item>>>, ApiError> { let items = sqlx::query_as!( Item, r#"SELECT id, name, quantity, created_at FROM items ORDER BY id"# ) .fetch_all(&pool) .await?; Ok(Json(ApiResponse { data: items })) } |
4. POST(レコード作成)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#[derive(Deserialize)] struct CreateItem { name: String, quantity: i32, } async fn create_item( Extension(pool): Extension<AnyPool>, Json(payload): Json<CreateItem>, ) -> Result<(axum::http::StatusCode, Json<ApiResponse<Item>>), ApiError> { let rec = sqlx::query_as!( Item, r#" INSERT INTO items (name, quantity) VALUES ($1, $2) RETURNING id, name, quantity, created_at "#, payload.name, payload.quantity ) .fetch_one(&pool) .await?; Ok(( axum::http::StatusCode::CREATED, Json(ApiResponse { data: rec }), )) } |
5. PUT と DELETE
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#[derive(Deserialize)] struct UpdateItem { name: Option<String>, quantity: Option<i32>, } async fn update_item( Extension(pool): Extension<AnyPool>, Path(id): Path<i64>, Json(payload): Json<UpdateItem>, ) -> Result<Json<ApiResponse<Item>>, ApiError> { // 動的にセットするだけの簡易例 let rec = sqlx::query_as!( Item, r#" UPDATE items SET name = COALESCE($1, name), quantity = COALESCE($2, quantity) WHERE id = $3 RETURNING id, name, quantity, created_at "#, payload.name, payload.quantity, id ) .fetch_one(&pool) .await?; Ok(Json(ApiResponse { data: rec })) } async fn delete_item( Extension(pool): Extension<AnyPool>, Path(id): Path<i64>, ) -> Result<axum::http::StatusCode, ApiError> { sqlx::query!("DELETE FROM items WHERE id = $1", id) .execute(&pool) .await?; Ok(axum::http::StatusCode::NO_CONTENT) } |
これらを Router に登録すれば、シンプルかつ型安全な CRUD API が完成します。
SeaORM を用いた実装との比較ポイント
sqlx と SeaORM はどちらも成熟したクレートですが、開発規模やチームのスキルセットに応じて選択すべき特徴があります。この節では重複しがちな記述を整理し、**実務で判断材料になる要点だけをピックアップしました。
主な相違点(表形式)
| 観点 | sqlx | SeaORM |
|---|---|---|
| クエリの書き方 | 生 SQL + query! / query_as! マクロで型安全 |
Entity/ActiveModel のメソッドチェーンで宣言的 |
| マイグレーション管理 | sqlx-cli が単体で提供 |
sea-orm-migration が統合的にサポート |
| パフォーマンス | 手書き SQL の分、若干高速 | ORM 層の抽象化に伴うオーバーヘッドが発生 |
| 学習コスト | Rust と SQL の両方を理解すれば開始可能 | Entity/ActiveModel, Relation の概念が必要 |
| 大規模開発での利点 | 柔軟性と最適化しやすさ | モデル中心設計でコードベースが統一されやすい |
どちらを選ぶべきか – 判断指針
- パフォーマンス重視・SQL が得意 →
sqlxを採用。 - モデル駆動開発・チームで共有したい →
SeaORMが適切。 - マイグレーションだけは同一ツールで管理したい →
SeaORMのmigrationパッケージが便利です。
実装例はそれぞれ公式リポジトリに豊富に用意されているので、試作段階で小さなサンプルを走らせながら感触を掴むと良いでしょう。
Docker Compose と CI/CD の実践的構成
コンテナ化はローカル開発と本番環境の差異をなくす最も手軽な方法です。ここでは PostgreSQL と MySQL の両方に対応できる docker-compose.yml、その上で動く cargo-watch 設定、さらに GitHub Actions を使った CI パイプラインの全体像を示します。
docker‑compose.yml(PostgreSQL デフォルト)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
version: "3.9" services: db: # デフォルトは PostgreSQL。MySQL に変える場合は image と環境変数を書き換えてください。 image: postgres:16-alpine environment: POSTGRES_USER: user POSTGRES_PASSWORD: secret POSTGRES_DB: axum_db ports: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data api: build: . command: cargo watch -x 'run' environment: # DB の種類に応じて URL を変更。${DB_TYPE} は .env で定義しても OK。 DATABASE_URL: postgres://user:secret@db:5432/axum_db ports: - "8080:8080" depends_on: - db volumes: pg_data: |
MySQL に切り替える手順
docker-compose.ymlのdbサービスのimageをmysql:8.0に変更。- 環境変数名を MySQL 用に書き換える(例:
MYSQL_ROOT_PASSWORD,MYSQL_DATABASEなど)。 apiコンテナのDATABASE_URLをmysql://user:secret@db:3306/axum_dbに置換。
このように 1 行だけ修正 すれば、同一コードベースで両方の DB を試せます。
Dockerfile(Rust 開発環境)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# ビルドステージ FROM rust:latest AS builder WORKDIR /app COPY Cargo.toml Cargo.lock ./ RUN cargo fetch # 依存関係だけ先に取得してキャッシュを有効化 COPY src ./src RUN apt-get update && apt-get install -y libssl-dev pkg-config RUN cargo build --release # 実行ステージ(軽量イメージ) FROM debian:bookworm-slim WORKDIR /app COPY --from=builder /app/target/release/axum-db-sample . EXPOSE 8080 CMD ["./axum-db-sample"] |
cargo watch -x 'run' がコンテナ起動時に自動実行され、コード変更があるたびに再ビルド・再起動します。
GitHub Actions(CI)全体像
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
name: CI on: push: branches: [ main ] pull_request: jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_USER: user POSTGRES_PASSWORD: secret POSTGRES_DB: axum_db ports: ["5432:5432"] options: >- --health-cmd="pg_isready -U user" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v4 - name: Install Rust toolchain run: rustup update stable && rustup default stable - name: Cache cargo registry uses: actions/cache@v3 with: path: ~/.cargo/registry key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo- - name: Install sqlx-cli (for migrations) run: cargo install sqlx-cli --features postgres,sqlite,mysql - name: Apply migrations env: DATABASE_URL: postgres://user:secret@127.0.0.1:5432/axum_db run: sqlx migrate run - name: Run tests env: DATABASE_URL: postgres://user:secret@127.0.0.1:5432/axum_db run: cargo test --all-features --locked |
テストコード例(integration test)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#[tokio::test] async fn integration_create_item() { // DB 接続は環境変数から取得 let pool = sqlx::PgPool::connect(&std::env::var("DATABASE_URL").unwrap()) .await .expect("プール作成失敗"); // ハンドラを直接呼び出す形でテスト let payload = serde_json::json!({ "name": "integration", "quantity": 5 }); let resp = crate::handlers::create_item( Extension(pool.clone()), axum::Json(payload), ) .await .expect("ハンドラ実行失敗"); assert_eq!(resp.0, axum::http::StatusCode::CREATED); } |
このテストは CI 環境でも同一データベースに対してエンドポイントロジックを検証でき、コードカバレッジの向上に寄与します。
まとめ
- 開発環境は
rustupとcargo-watchのみで完結し、変更即時リロードが実現できます。 - プロジェクト構成では
.envとdotenvyにより機密情報を安全に管理し、クレートはキャレット指定で将来のアップデートにも耐えられる形にしています。 - マイグレーションは
sqlx-cli(または SeaORM の migration)でバージョン管理し、AnyPoolを使って DB 種類横断的な接続プールを Axum に注入します。 - CRUD 実装は
query!系マクロと統一エラーハンドリングで型安全かつ見通しの良いコードが書けます。 - SeaORM との比較では、パフォーマンス・学習コスト・チーム規模に応じた選択指針を提示しました。実際に小さなサンプルで試すと判断材料が得られます。
- Docker Compose と CIは PostgreSQL をデフォルトにしつつ、MySQL へもワンクリックで切り替え可能な設定例を示しました。GitHub Actions による自動マイグレーション+テスト実行で、プルリクエスト時の品質保証が容易になります。
以上の手順とベストプラクティスに従えば、Rust + Axum で安全かつスケーラブルなデータベース駆動 API を短時間で構築できるはずです。ぜひ本稿のサンプルリポジトリをクローンし、自分のプロジェクトへ組み込んで実践してみてください。