Rust×Axumで作るREST API ③ SeaORMでDB接続・CRUD完成

はじめに

本記事は、Rust と Axum で REST API を構築するシリーズの第3回(最終回)です。前回のルーティング・ミドルウェア編では、エンドポイントの整理とログ出力・CORS の導入までを実装しました。

Rust×Axumで作るREST API ② ルーティング・ミドルウェアの実装
はじめに本記事では、Rust Axum ルーティング ミドルウェアをテーマに、複数ルートの定義からロギングミドルウェアの実装までを解説します。シリーズの第2回として、第1回で作った最小構成の API ...

今回はその続きとして、Rust に SeaORM を組み合わせ、Axum で CRUD のエンドポイントを一通り実装して API を完成させます。

本シリーズで扱った内容は以下の通りです。

この記事を読み終える頃には、SeaORM 経由で SQLite に永続化したタスクを、作成・取得・更新・削除の全エンドポイントから操作できる状態になります。

本記事の対象は、第2回までを通して Axum の基本を把握したエンジニアです。SeaORM 単体の使い方はRust×SeaORMで学ぶORM入門で解説しているため、合わせて参考にしてください。

Rust×SeaORMで学ぶORM入門:セットアップと基本操作を解説
はじめに今回は、Rustの人気ORMパッケージであるSeaORMをインストールから、簡単なデータベース操作までを解説します。なお、今回紹介するソースコードはこちらにあります。また、Rustのデスクトッ...

全体像とDBスキーマ

本記事では、シンプルなタスク管理 API を題材に、Axum と SeaORM で作成・取得・更新・削除のエンドポイントを実装します。扱うリソースは以下のスキーマを持つ tasks テーブルです。

  • id:i32 / 主キー / オートインクリメント
  • title:String / タスク名
  • completed:bool / 完了フラグ

実装するエンドポイントは以下の5つです。

  • タスクの作成(tasks への作成リクエスト)
  • タスク一覧の取得(tasks への取得リクエスト)
  • 指定IDのタスク取得(tasks 配下のID指定取得リクエスト)
  • タスクの更新(tasks 配下のID指定更新リクエスト)
  • タスクの削除(tasks 配下のID指定削除リクエスト)

DB は手元で動かしやすいよう SQLite を使います。本番では PostgreSQL や MySQL に切り替え可能です。

SeaORM の依存追加とエンティティ定義

まずは、Cargo.toml に SeaORM と関連クレートを追加します。


[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
sea-orm = { version = "0.12", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] }
serde = { version = "1", features = ["derive"] }

次に、テーブルに対応するエンティティを定義します。src/entity/task.rs に、Model を以下のように記述します。


pub struct Model {
    pub id: i32,
    pub title: String,
    pub completed: bool,
}

実際には SeaORM のマクロを使い、tasks テーブルとの対応付けや主キーの指定、JSON 変換の派生などを上記の Model に付与します。この属性付与により、後続のコードで使う task::Entity(クエリの起点となる型)や task::ActiveModel(INSERT・UPDATE 用の可変オブジェクト)が自動生成されます。具体的な属性の書き方は公式ドキュメントを参照してください。

SeaORM 🐚 An async & dynamic ORM for Rust
🐚 SeaORM is a relational ORM to help you build web services in Rust

続いて、src/entity/mod.rs を作成し、モジュールを公開します。


pub mod task;

DB接続とアプリケーションステート

SeaORM の接続オブジェクトは、Axum のアプリケーションステートとしてハンドラに渡せるようにします。以下の処理を src/main.rs に記述します。


use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::get,
    Json, Router,
};
use sea_orm::{
    ActiveModelTrait, ConnectionTrait, Database, DatabaseConnection, EntityTrait, Schema, Set,
};
use serde::Deserialize;

mod entity;
use entity::task;

#[derive(Clone)]
struct AppState {
    db: DatabaseConnection,
}

#[tokio::main]
async fn main() {
    // SQLiteに接続(ファイルがなければ作成される)
    let database_url = "sqlite://./tasks.db?mode=rwc";
    let db = Database::connect(database_url)
        .await
        .expect("DB接続に失敗しました");

    // tasksテーブルを作成(既に存在する場合はエラーを無視)
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let stmt = schema.create_table_from_entity(task::Entity);
    let _ = db.execute(backend.build(&stmt)).await;

    let state = AppState { db };

    // ルーティングは次のセクションで定義
    // ... 省略
}

SQLite の接続文字列に読み書き+作成のモード指定を付けることで、ファイルがない場合に自動作成されるようになります。Schema::create_table_from_entity を使うことで、エンティティ定義からテーブルを生成できます。

注意:本番環境では sea-orm-migration を使ったマイグレーション管理がおすすめです。本記事では学習用に簡易な方法を採用しています。

CRUDエンドポイントの実装

続いて、5つのハンドラを実装します。src/main.rsmain 関数の下に追記してください。まずはリクエストボディの型を2つ定義します。


#[derive(Deserialize)]
struct CreateTask {
    title: String,
}

#[derive(Deserialize)]
struct UpdateTask {
    title: Option,
    completed: Option,
}

続いて、タスクを作成する CREATE のハンドラです。


async fn create_task(
    State(state): State,
    Json(payload): Json,
) -> Result, StatusCode> {
    let new_task = task::ActiveModel {
        title: Set(payload.title),
        completed: Set(false),
        ..Default::default()
    };
    let result = new_task
        .insert(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(Json(result))
}

次に、タスクの取得(READ)を一覧と1件の2つに分けて実装します。


// 一覧取得
async fn list_tasks(
    State(state): State,
) -> Result>, StatusCode> {
    let tasks = task::Entity::find()
        .all(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(Json(tasks))
}

// 1件取得
async fn get_task(
    State(state): State,
    Path(id): Path,
) -> Result, StatusCode> {
    let task = task::Entity::find_by_id(id)
        .one(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .ok_or(StatusCode::NOT_FOUND)?;
    Ok(Json(task))
}

続いて、タスクを更新する UPDATE のハンドラです。


async fn update_task(
    State(state): State,
    Path(id): Path,
    Json(payload): Json,
) -> Result, StatusCode> {
    let target = task::Entity::find_by_id(id)
        .one(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .ok_or(StatusCode::NOT_FOUND)?;

    let mut active: task::ActiveModel = target.into();
    if let Some(title) = payload.title {
        active.title = Set(title);
    }
    if let Some(completed) = payload.completed {
        active.completed = Set(completed);
    }
    let updated = active
        .update(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(Json(updated))
}

UPDATE では、まず対象レコードを取得してから ActiveModel に変換し、変更したいフィールドだけ Set で上書きしています。これにより、JSONで渡されなかったフィールドは元の値が維持されます。

最後に、タスクを削除する DELETE のハンドラです。


async fn delete_task(
    State(state): State,
    Path(id): Path,
) -> Result {
    task::Entity::delete_by_id(id)
        .exec(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(StatusCode::NO_CONTENT)
}

最後に、main 関数末尾の // ルーティングは次のセクションで定義 以下を、以下のコードに置き換えます。


let app = Router::new()
    .route("/tasks", get(list_tasks).post(create_task))
    .route(
        "/tasks/:id",
        get(get_task).put(update_task).delete(delete_task),
    )
    .with_state(state);

let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
    .await
    .unwrap();
println!("サーバーを起動しました: http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();

これで、/tasks リソースに対する CRUD エンドポイントが SeaORM 経由で揃いました。

動作確認

実装が終わったので、動作確認をしていきます。まずは、以下のコマンドでサーバーを起動します。


cargo run

続いて、別のターミナルから順番にエンドポイントを叩いていきます。

まずは、リクエストボディに使う JSON ファイルを作業ディレクトリに2つ用意してください。

  • create.json:タイトル「記事を書く」を持つタスクの作成用ペイロード
  • update.json:完了フラグを true にする更新用ペイロード

内容はそれぞれ titlecompleted のフィールドを持つシンプルなオブジェクトです。お好みのエディタで作成してください。

タスクの作成は以下のコマンドで行います。


curl --request POST http://127.0.0.1:3000/tasks \
  --header "Content-Type: application/json" \
  --data @create.json

一覧と1件の取得は以下の通りです。


curl http://127.0.0.1:3000/tasks
curl http://127.0.0.1:3000/tasks/1

更新は、以下のコマンドで送信します。


curl --request PUT http://127.0.0.1:3000/tasks/1 \
  --header "Content-Type: application/json" \
  --data @update.json

削除は、以下のコマンドで実行します。


curl --request DELETE http://127.0.0.1:3000/tasks/1

作成リクエストのレスポンス例:


{"id":1,"title":"記事を書く","completed":false}

更新後の取得リクエストのレスポンス例:


{"id":1,"title":"記事を書く","completed":true}

まとめ

今回は、Axum と SeaORM で全エンドポイントを実装し、シリーズで構築してきた REST API を完成させました。これにより、DB に永続化したタスクリソースを REST API 経由で操作できる状態になりました。

本シリーズを通して、Axum の基本セットアップから、ルーティング設計・ミドルウェア導入・SeaORM での CRUD まで一通り体験できたと思います。実務では、認証・バリデーション・テストなど追加で考慮すべき点がいくつかあります。気になる方はSeaORM 公式ドキュメントや Axum のリファレンスを合わせて確認してください。

SeaORM 🐚 An async & dynamic ORM for Rust
🐚 SeaORM is a relational ORM to help you build web services in Rust

SeaORM の基本操作をもう一度押さえておきたい方は、Rust×SeaORMで学ぶORM入門もご覧ください。

Rust×SeaORMで学ぶORM入門:セットアップと基本操作を解説
はじめに今回は、Rustの人気ORMパッケージであるSeaORMをインストールから、簡単なデータベース操作までを解説します。なお、今回紹介するソースコードはこちらにあります。また、Rustのデスクトッ...
タイトルとURLをコピーしました