メインコンテンツへスキップ
見出し画像

【Rust】ORM「Diesel」を使用したDB操作APIを作ってみた【AWS Lambda】

    はじめに

    こんにちは!
    2025年4月に別業界からネオス株式会社に飛び込んだ30過ぎのおじさんです。

    先日は所属する開発技術本部SA部のメンバーと共に、

    【Rust】ノーコードでAWS Lambda + API構築 + DB操作をやってみた【前編】/【後編】

    として、Rust言語でDynamoDBを操作するAPIを作成してみました。

    その後、一般的なRDS(今回はMySQL)をRust言語を使って操作しようと試行錯誤した結果、たどり着いたORM「Diesel」をご紹介したいと思います!

    この記事は以下のような方におすすめです!

    • Rustに興味のある方や学び始めている方

    • DB操作を軽量化させてみたい方

    • AWS上に軽量なAPIやバッチ処理を構築してみたい方

    ぜひご覧ください。

    【大事な余談】
    2025年11月14日、AWS LambdaでRustが公式サポートされました!

    RustでDB操作したい!

    Rustって、いいよね!

    以前の記事でも紹介したRust言語のメリットのおさらいです。

    1. 高速な実行性能(C/C++に近いパフォーマンス)

    2. メモリ安全性をコンパイル時に保証(所有権・借用チェックによる)

    3. データ競合を防ぐ並行性モデル

    4. パッケージ管理(Cargo)が標準で強力

    5. 豊富な型システムによるバグの早期発見

    6. 安全性と速度を両立したシステムプログラミングが可能

    (参照: https://rust-lang.org/ja/ )

    このメリットで前回は高性能なAWS Lambda関数を作成しましたが、特に生成AIを用いたコーディングではコンパイルの際に不具合に気づけることで、生成AIとのエラーメッセージの会話ラリーだけでプロジェクトを(ほぼ)完成させられるという唯一無二のメリットがあります!

    RDSとSQL文…

    ただ、一般的なRDSはSQL文を用いて指示を出す必要があり、どの言語のプロジェクトでも普通は

    • SQL文のテンプレートを準備する

    • SQL文のテンプレートに変数を代入してRDSに指示を出すコードを記述する

    • RDSから返ってきた値を受け取るコードを記述する

    という手順が必要となります。

    ですが、Rust言語のプロジェクトだと、SQL文はコンパイル時にチェックしてくれないので…

    • RDSスキーマと異なる型の変数を代入したSQL文でRDSに指示を出してしまう

    • RDSから返ってきた値がRustプロジェクトの想定していない型だった

    • そもそもSQL文の記述方法が間違っている

    という場合、実際にRDSを立ち上げて、コンパイルされたプロジェクトを走らせるまで不具合に気づきません…。
    これじゃあ、「Rust × 生成AI」のメリットが半減です…。
    困った…。

    ここで「Diesel」!

    困った困ったと調べているとRustのORM「Diesel」に行きつきました!
    Diesel(公式サイト)は、Rust向けに開発された型安全性の高いORM(Object-Relational Mapping)ライブラリで以下の特徴があります!

    1. PostgreSQL・MySQL・SQLiteに対応

    2. コンパイル時にSQLクエリの整合性チェックを行う仕組みを備えており、スキーマとコードの不一致を早期に検出可能

    3. マクロやコード生成機能を利用して、Rust側の型とデータベースのスキーマを同期させやすい

    4. クエリビルダを備えており、Rustの型システムに基づいた記述でSQLと同等の操作を実装できる

    5. 必要に応じて生SQLも実行でき、型安全な操作と柔軟な操作を併用できる

    Rustの型システムを活かした安全性があり、データベーススキーマとの整合性までチェックできることが大きな強みとなります。

    つまり、コンパイルした際に先ほどの不具合に気づくことができるということです!
    これは生成AIコーディングに、ぴったり!

    今回、こんなAPI・バッチ処理の作ってみました

    画像
    ※今回紹介する部分を赤字にしています!

    メソッドパスリクエストボディ処理内容GET/fruitsなし全フルーツの一覧を取得POST/fruitsfruit, price新しいフルーツを追加GET/fruits/{fruit_name}なし指定したフルーツの詳細を取得PUT/fruits/{fruit_name}price指定したフルーツ情報を更新DELETE/fruits/{fruit_name}なし指定したフルーツを削除POST/fruits/timeSaleprice, discount_rate基準価格より高いフルーツを割引率引きとする\scriptsize \begin{array} {r:l:l:l} \textbf{メソッド} & \textbf{パス} & \textbf{リクエストボディ} & \textbf{処理内容} \\ \hline \text{GET} & \text{/fruits} & \text{なし} & \text{全フルーツの一覧を取得} \\ \hline \text{POST} & \text{/fruits} & \text{fruit,\ price} & \text{新しいフルーツを追加} \\ \hline \text{GET} & \text{/fruits/\{fruit\_name\}} & \text{なし} & \text{指定したフルーツの詳細を取得} \\ \hline \text{PUT} & \text{/fruits/\{fruit\_name\}} & \text{price} & \text{指定したフルーツ情報を更新} \\ \hline \text{DELETE} & \text{/fruits/\{fruit\_name\}} & \text{なし} & \text{指定したフルーツを削除} \\ \hline \text{POST} & \text{/fruits/timeSale} & \text{price,\ discount\_rate} & \text{基準価格より高いフルーツを割引率引きとする} \\ \hline \end{array}

    ※タイムセール機能は、指定時間に起動するバッチ処理で解除します!

    なお、当社開発技術本部ではClaude Codeが導入されました!
    自社プロダクトはもちろん、受託プロダクトでもクライアントから許可を頂けたものはClaude Codeを用いたコーディングを行っています!

    早速、本記事もClaude Codeを活用してみました!

    作ってみた

    RDS構築(割愛)

    公式サイトやhon hon様の記事等を参照に、以下の内容でAurora MySQLの設定を行いました!

    Aurora MySQLの設定

    • インスタンスタイプ:db.r6g.large(メモリ最適化クラス)

    • 可用性と耐久性:Aurora レプリカを作成しない ※試験利用のため

    • 接続

      • コンピューティングリソース:EC2 コンピューティングリソースに接続しない

      • ネットワークタイプ:IPv4

      • VPC セキュリティグループを指定し、特定のセキュリティグループのLambda関数の3306ポートのみ接続可能にする

    また、 今回のAPI・バッチ処理のイメージ に合わせて、以下のスキーマでRDSを構築しました!

    スキーマ
    Rust_greengrocer_DDL_mysql.sql

    CREATE DATABASE IF NOT EXISTS `rustdb`;
    
    USE `rustdb`;
    
    CREATE TABLE `greengrocer` (
      `id` int auto_increment NOT NULL COMMENT 'ID',
      `name` varchar(32) NOT NULL COMMENT 'フルーツ名',
      `price` int NOT NULL COMMENT '価格',
      `tax_rate` decimal(4,2) NOT NULL DEFAULT 0.08 COMMENT '税率',
      `original_price` int DEFAULT NULL COMMENT '値下げ前の価格',
      `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
      `updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日時',
      PRIMARY KEY (`id`),
      UNIQUE KEY `greengrocer_IX` (`name`)
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = '八百屋';

    さて、Rustのソースコードを記述していきます!

    まずは cargo add

    今回は前回の記事(前編)のソースコードを書き換える方向で記述したいと思います。
    公式サイト(Getting Started with Diesel)によると、依存関係( Cargo.toml )に diesel と dotenvy が必要となるので、以下のコマンドで追加します。

    cargo add diesel --features mysql
    cargo add dotenvy

    Claude Codeを活用してソースコードを書いてみる

    ここからが本題です!
    マークダウンを作成してClaude Codeに指示をしたり、1つ1つプロンプトで指示をしても良いのですが、Claude Codeはプロジェクトを読み取ってソースコードを作成することに特化しているため、面倒くさがりな 私はソースコードに具体的な指示を書いてしまいます!

    src/http_handler.rs(抜粋)

    (略)
        // MySQLとの接続
        // 以下はDynamoDBのコードだが、これをMySQLに置き換えたい
        let config = aws_config::load_from_env().await;
        let client = Client::new(&config);
    (略)
        // MySQLのtable_nameからfruit_nameの情報を取得
        // 以下はDynamoDBのコードだが、これをMySQLに置き換えたい
        let resp = client
            .query()
            .table_name("greengrocer")
            .key_condition_expression("#name = :fruit")
            .expression_attribute_names("#name", "name")
            .expression_attribute_values(":fruit", AttributeValue::S(fruit_name.to_string()))
            .send()
            .await?;
    (略)

    これで、Claude Codeに指示しましょう!
    ※以下、本記事では「claude-haiku-4-5-20251001(Claude Haiku 4.5)」モデル使用しました。

    ▶ src/http_handler.rs のコメントと Rust_greengrocer_DDL_mysql.sql を参考にして、DynamoDBではなくMySQLを操作するように src/http_handler.rs を書き換えて。その際、Dieselを用いて、基本的にSQL文を使用せずに記述して。

    跡形もなく書き換えられてしまいました…。

    ここからがRustとDieselの本領発揮です!
    その後、cargo testのコマンドを入力するとエラーメッセージが出てくるので、それをClaude Codeに投げます。
    計8回エラーメッセージを投げることで、cargo testでエラーメッセージが表示されなくなりました…!

    ※また、Claude Codeには自力でコマンドラインを操作してくれる機能もあり、このデバッグラリーすら行わないで基本的なデバッグを完了させてくれます!

    生成AIによるコーディングはデバッグとの戦い…ですが、Rust × Diesel × 生成AIの組み合わせで、簡単にデバッグを行うことができました!

    これは、Dieselが、

    • Rustの型システムを活かした安全性

    • データベーススキーマとの整合性までチェックする機能

    を備えているお陰です!

    さらに機能を追加してみた

    このままでは前回と同じ機能しかないので、少し手を加えてみます!

    ▶ POSTメソッドかつfruit_nameがtimeSaleの場合、リクエストボディのpriceを超えるRDSのpriceの商品をRDSから全て取得して、original_priceが空の場合その商品のpriceをoriginal_priceに格納して、リクエストボディのdiscount_rate(f64型)とRDSのpriceを使って「(1-discount_rate)*price」(小数点以下切り捨て)をpriceに格納して、RDSを更新する処理を追加して。

    さらに

    ▶ cancel-time-sale直下にタイムセール状態を解除するバッチ処理を以下の内容で追加したい。

    • APIプロジェクトと同様、Rust言語でDieselを用いて、SQL文を使用せずに記述すること

    • AWS Lambdaにデプロイして、毎日定刻に起動するように設定する前提で記述すること

    • Rust_greengrocer_DDL_mysql.sqlで設定されたRDSで、original_priceに値が入っている項目を全て取得して、priceにoriginal_priceを代入し、original_priceをNullとすること

    作ってみたソースコードは以下の通りです!

    lambda-template-rust/
    ├── src/
    │   ├── http_handler.rs
    │   ├── main.rs
    │   ├── models.rs
    │   └── schema.rs
    ├── target/
    ├── .gitignore
    ├── Cargo.lock
    ├── Cargo.toml
    ├── README.md
    ├── .README.md
    ├── cancel-time-sale/ ※バッチ処理用の別プロジェクト内包
    │   ├── src/
    │   │   ├── main.rs
    │   │   ├── models.rs
    │   │   └── schema.rs
    │   ├── target/
    │   ├── Cargo.lock
    │   └── Cargo.toml
    └── Rust_greengrocer_DDL_mysql.sql ※Claude Codeでソースコード生成に使用

    以下、画像リンクからGitHub Gistに飛べます!

    画像
    画像
    画像
    画像
    画像
    画像
    画像
    画像
    画像

    Dieselのコードを見てみよう

    ここで「Diesel」!\textit{ここで「Diesel」!}の通り、Dieselの一番の特徴としてRustの型システムに基づいた記述でSQLと同等の操作を実装できることが挙げられます。
    実際にコードを見てみましょう!

    1.データベーススキーマ作成

    まず、使用するテーブルだけ(超重要!)、schema.rsに記述します。

    src/schema.rs

    // @generated automatically by Diesel CLI.
    
    diesel::table! {
        greengrocer (id) {
            id -> Integer,
            name -> Varchar,
            price -> Integer,
            tax_rate -> Double,
            original_price -> Nullable<Integer>,
            created -> Timestamp,
            updated -> Timestamp,
        }
    }

    今回、全てClaude Codeに書かせてしまいましたが、

    3.マクロやコード生成機能を利用して、Rust側の型とデータベースのスキーマを同期させやすい

    と記載した通り、Diesel CLI(公式GitHub)を用いることで、RDSと接続してマイグレーションを行い、自動でschema.rsを生成する機能もあります。
    (今回の本題とズレるので、こちらをご参照ください!)

    また、使用するカラムだけ(超重要!)、models.rsに記述します。

    src/models.rs

    use diesel::prelude::*;
    use crate::schema::greengrocer;
    
    // 読み取り用
    #[derive(Queryable, Debug, Clone)]
    pub struct Greengrocer {
        pub id: i32,
        pub name: String,
        pub price: i32,
        pub tax_rate: f64,
        pub original_price: Option<i32>,
    }
    
    // 新規挿入用
    #[derive(Insertable)]
    #[diesel(table_name = greengrocer)]
    pub struct NewGreengrocer {
        pub name: String,
        pub price: i32,
        pub tax_rate: f64,
        pub original_price: Option<i32>,
    }

    2.コネクションプールの確立

    ここはお作法のようなところです。
    ・ユーザ名
    ・パスワード
    を含んだデータベースURLを指定して、接続します。

    今回はAWS Lambdaの環境変数で1種類のみ指定しますが、
    ・更新用と読み取り専用の区別
    ・AWS Secrets Manager の使用
    を本番環境では検討したいところです。

    src/main.rs(抜粋)

    
    type DbPool = Arc<Mutex<MysqlConnection>>;
    
        let database_url = std::env::var("DATABASE_URL")
            .unwrap_or_else(|_| "mysql://root:password@localhost/rustdb".to_string());
    
        let connection = MysqlConnection::establish(&database_url)
            .expect("Failed to establish database connection");
    
        let pool = Arc::new(Mutex::new(connection));
    

    3.データの取得

    基本的なSQL文の構造に準じていて、

    • schema.rs で記述したテーブルを greengrocer::table で指定する

    • .filter() で条件を指定する

    • .select() で欲しいカラムを指定する

    • .first::<型>(コネクション) なら1件、 .load::<型>(コネクション) なら複数件取得する

    といった形です。
    なお、 .filter() の前に .inner_join(another_table.on(greengrocer::name.eq(another_table::name))) 等記述すれば、テーブルの結合も行うことができます!

    src/http_handler.rs(抜粋)

    // フルーツ情報取得
        let result = tokio::task::block_in_place(|| {
            let mut conn = db_pool.lock().unwrap();
            greengrocer::table
                .filter(greengrocer::name.eq(&fruit_name))
                .select((greengrocer::id, greengrocer::name, greengrocer::price, greengrocer::tax_rate, greengrocer::original_price))
                .first::<Greengrocer>(&mut *conn)
        });
    
    // フルーツ情報更新
        let result = tokio::task::block_in_place(|| {
            let mut conn = db_pool.lock().unwrap();
    
            // フルーツが存在するか確認
            let exists: bool = greengrocer::table
                .filter(greengrocer::name.eq(&fruit_name))
                .select(greengrocer::id)
                .first::<i32>(&mut *conn)
                .optional()
                .unwrap_or(None)
                .is_some();
        });
    
    // フルーツ情報一覧取得
        let result = tokio::task::block_in_place(|| {
            let mut conn = db_pool.lock().unwrap();
            greengrocer::table
                .select((greengrocer::id, greengrocer::name, greengrocer::price, greengrocer::tax_rate, greengrocer::original_price))
                .load::<Greengrocer>(&mut *conn)
        });
    
    // タイムセール価格更新
        let result = tokio::task::block_in_place(|| {
            let mut conn = db_pool.lock().unwrap();
    
            // リクエストボディのpriceを超えるRDSのpriceの商品を全て取得
            let items: Vec<Greengrocer> = greengrocer::table
                .filter(greengrocer::price.gt(body.price as i32))
                .select((greengrocer::id, greengrocer::name, greengrocer::price, greengrocer::tax_rate, greengrocer::original_price))
                .load::<Greengrocer>(&mut *conn)
                .map_err(|e| format!("Failed to load items: {:?}", e))?;
        });

    cancel-time-sale/src/main.rs(抜粋)

        // original_priceに値が入っている項目を全て取得
        let items: Vec<Greengrocer> = greengrocer::table
            .filter(greengrocer::original_price.is_not_null())
            .select((
                greengrocer::id,
                greengrocer::name,
                greengrocer::price,
                greengrocer::tax_rate,
                greengrocer::original_price,
            ))
            .load::<Greengrocer>(&mut conn)
            .expect("Failed to load items");

    4.データの更新・削除
    3.データの取得\textit{3.データの取得} に加えて、

    • .update(テーブル.filter()).set(更新内容) で更新内容を指定する

    • .delete(テーブル.filter()) でレコード削除を指定する

    • .insert_into(テーブル).values(レコードの内容) でレコード削除を指定する

    • .execute(コネクション) で実行する

    という形となります!
    RDSに触れたことのある方なら、よく読めばすんなり理解できるのでは…?

    src/http_handler.rs(抜粋)

    // フルーツ情報更新
            // 更新
            diesel::update(greengrocer::table.filter(greengrocer::name.eq(&fruit_name)))
                .set((
                    greengrocer::price.eq(body.price as i32),
                    greengrocer::tax_rate.eq(TAX_RATE),
                ))
                .execute(&mut *conn)
                .map_err(|e| format!("Update failed: {:?}", e))
    
    // フルーツ情報削除
        let result = tokio::task::block_in_place(|| {
            let mut conn = db_pool.lock().unwrap();
            diesel::delete(greengrocer::table.filter(greengrocer::name.eq(&fruit_name)))
                .execute(&mut *conn)
        });
    
    // タイムセール価格更新
                // RDSを更新
                diesel::update(greengrocer::table.filter(greengrocer::id.eq(item.id)))
                    .set((
                        greengrocer::price.eq(new_price),
                        greengrocer::original_price.eq(new_original_price),
                    ))
                    .execute(&mut *conn)
                    .map_err(|e| format!("Failed to update item: {:?}", e))?;
    
    // フルーツ情報登録
        let result = tokio::task::block_in_place(|| {
            let mut conn = db_pool.lock().unwrap();
            diesel::insert_into(greengrocer::table)
                .values(&new_fruit)
                .execute(&mut *conn)
        });

    cancel-time-sale/src/main.rs(抜粋)

                // priceにoriginal_priceを代入し、original_priceをNullに設定
                diesel::update(greengrocer::table.filter(greengrocer::id.eq(item.id)))
                    .set((
                        greengrocer::price.eq(original_price),
                        greengrocer::original_price.eq(None::<i32>),
                    ))
                    .execute(&mut conn)
                    .expect("Failed to update item");

    実装・動作確認(割愛)

    APIは、前回の記事(前編・後編)を参考に、AWS Lambdaにデプロイします。
    この際、
     ・環境変数
     ・VPC セキュリティグループ
    の設定を忘れずに!

    そして、下記の表の通りに動作すれば、OKです!

    メソッドパスリクエストボディ処理内容GET/fruitsなし全フルーツの一覧を取得POST/fruitsfruit, price新しいフルーツを追加GET/fruits/{fruit_name}なし指定したフルーツの詳細を取得PUT/fruits/{fruit_name}price指定したフルーツ情報を更新DELETE/fruits/{fruit_name}なし指定したフルーツを削除POST/fruits/timeSaleprice, discount_rate基準価格より高いフルーツを割引率引きとする\scriptsize \begin{array} {r:l:l:l} \textbf{メソッド} & \textbf{パス} & \textbf{リクエストボディ} & \textbf{処理内容} \\ \hline \text{GET} & \text{/fruits} & \text{なし} & \text{全フルーツの一覧を取得} \\ \hline \text{POST} & \text{/fruits} & \text{fruit,\ price} & \text{新しいフルーツを追加} \\ \hline \text{GET} & \text{/fruits/\{fruit\_name\}} & \text{なし} & \text{指定したフルーツの詳細を取得} \\ \hline \text{PUT} & \text{/fruits/\{fruit\_name\}} & \text{price} & \text{指定したフルーツ情報を更新} \\ \hline \text{DELETE} & \text{/fruits/\{fruit\_name\}} & \text{なし} & \text{指定したフルーツを削除} \\ \hline \text{POST} & \text{/fruits/timeSale} & \text{price,\ discount\_rate} & \text{基準価格より高いフルーツを割引率引きとする} \\ \hline \end{array}

    また、バッチ処理のAWS Lambdaへのデプロイは、EventBridgeをトリガーに追加して、Cron またはRate式でスケジュール指定すればOKです!

    スケジュール指定例: rate(1 day), cron(0 17 ? * MON-FRI *)

    デプロイできたら、指定の時間に動作するか、見てみましょう!

    終わりに

    いかがでしたか?
    RDSを組み合わせたシステムを構築する際にRust言語のORM「Diesel」は

    • RDSに触れたことのある方なら、すんなり理解できそうな記法

    • コンパイル時に整合性チェックを行うため、生成AIコーディングでもデバッグが簡単!

    • 軽量なデプロイパッケージとなるRust自体がAWS Lambdaの「低レイテンシ+低コスト運用」に最適!(前回の後編記事で確認済み!)

    ということが分かったのではないでしょうか?

    苦労してたどり着いたこのDieselで、私自身も次のプロジェクトの生成AIコーディングを行っていきます!

    長文となりましたが、ここまでご覧いただき、ありがとうございました。

    あなたへのおすすめ