🧚

Rust | Actix Web と slqx を使って TiDB Serverless にアクセスする

2024/08/12に公開

はじめに

今更ながら、ゼルダの時のオカリナにハマっている Shota ですw

今回は、Rust の webフレームワークの Actix Web ✖︎ sqlx ✖︎ TiDB で簡易的なAPIサーバーを構築しましたので、紹介をしていきます!
他の参考記事もほとんどなかったので、ぜひ本記事が参考になれば嬉しいです。

Actix Web については以前に公開した記事がありますので、こちらも参考になればと。
https://zenn.dev/collabostyle/articles/77f4f8528ded1c

🔻Actix Web
https://actix.rs/

🔻sqlx
https://docs.rs/sqlx/latest/sqlx/

🔻TiDB
https://pingcap.co.jp/tidb/

下準備

プロジェクトの作成

以下でプロジェクトを作成します。

cargo new my-app
cd my-app

プロジェクトフォルダ直下の、Cargo.toml に以下のクレート群を追加しておきます。
TiDB は MySQL 互換ですので、sqlx では MySQL のオプションを使いながら接続をします。

Cargo.toml
[dependencies]
actix-web = "4.8.0"
chrono = { version = "0.4.38", features = ["serde"] }
dotenv = "0.15.0"
serde = { version = "1.0.204", features = ["derive"] }
sqlx = { version = "0.8.0", features = [
  "mysql",
  "runtime-tokio-rustls",
  "migrate",
  "chrono",
] }

.env の作成

プロジェクトフォルダ直下に .envを作成し、TiDBのデータベースURLを定義しておきます。

.env
DATABASE_URL=mysql://{ユーザー名}.root:{パスワード}@gateway01.ap-northeast-1.prod.aws.tidbcloud.com:4000/test

テーブルとサンプルデータの作成

TiDB のクラスターページにアクセスし、左タブの「SQL Editor」で、TODO のテーブルとサンプルデータを詰め込んだ SQL を実行していきます。
TiDB

実行するSQL
CREATE TABLE `todos` (
  `id` VARCHAR(26) PRIMARY KEY,
  `title` VARCHAR(100) NOT NULL,
  `description` TEXT,
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

INSERT INTO
  `todos` (`id`, `title`, `description`)
VALUES
  (
    '01J507WHN8CQNGH04Q908RY4WX',
    '洗濯物干し',
    '雨が降る前に干して、回収するまで'
  ),
  (
    '01J507WWHH62D5CZPTAB7GJR8C',
    'おつかい',
    '野菜とトイレットペーパーをイオンで買ってくる。お母さんに怒られる前に'
  ),
  (
    '01J507X6XM3MCJ8VJVRESD4C2F',
    '宿題',
    '計算道場 P40 まで終わらせる。ゼッタイ'
  );

ここまででひとまず、下準備は完了です⭐️

コードの実装

簡易的な実装ということで、単一のエンドポイントを実装するところまでで終わりたいと思います。

  • GET /todos
    • TODO一覧の取得

コネクションの作成

sqlx と TiDB(MySQL)とのコネクションを作成していきます。
この関数は、最終的に、Pool<MySql> が返るように実装しました。

use std::env;

use sqlx::{MySql, MySqlPool, Pool};

// MySQL コネクションの作成
async fn get_db_pool() -> Pool<MySql> {
    dotenv::dotenv().expect("Failed to read .env file");
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    MySqlPool::connect(&database_url)
        .await
        .unwrap_or_else(|_| panic!("Cannot connect to the database"))
}

コネクションを使い、DBへのアクセスを行う

ここでは、先ほど作成した Pool<MySql>のコネクションを使って、DBにアクセスし、
todos テーブルから全ての情報を取得するという SQL を書いています。

use chrono::Utc;
use serde::Serialize;

#[derive(Serialize)]
struct Todos {
    id: String,
    title: String,
    description: Option<String>,
    created_at: chrono::DateTime<Utc>,
}

// TiDB からの取得
async fn fetch_all() -> Result<Vec<Todos>, sqlx::Error> {
    let db_pool = get_db_pool().await;
    sqlx::query_as!(
        Todos,
        r#"
        select
            id,
            title,
            description,
            created_at
        from
            todos
        "#,
    )
    .fetch_all(&db_pool)
    .await
}

ハンドラー関数と、main関数の作成

get_todos というハンドラー関数を作成し、fetch_all() で DB から取得してきた Todos をActix Web の応答形式に変換するという役割を担います。

最後に main関数を作成しておき、GET /todos がコールされる時の処理を記述しています。

use actix_web::{web, App, HttpResponse, HttpServer, Responder};

// ハンドラー
async fn get_todos() -> impl Responder {
    let todos = fetch_all().await;
    match todos {
        Ok(t) => HttpResponse::Ok().json(t),
        Err(e) => {
            println!("{e}");
            HttpResponse::NotFound().json("Not Found Error")
        }
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().route("/todos", web::get().to(get_todos)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

最終的には以下のようなコードになります。

最終的な全てのコード
main.rs
use std::env;

use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use chrono::Utc;
use serde::Serialize;
use sqlx::{MySql, MySqlPool, Pool};

// MySQL コネクションの作成
async fn get_db_pool() -> Pool<MySql> {
    dotenv::dotenv().expect("Failed to read .env file");
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    MySqlPool::connect(&database_url)
        .await
        .unwrap_or_else(|_| panic!("Cannot connect to the database"))
}

#[derive(Serialize)]
struct Todos {
    id: String,
    title: String,
    description: Option<String>,
    created_at: chrono::DateTime<Utc>,
}

// TiDB からの取得
async fn fetch_all() -> Result<Vec<Todos>, sqlx::Error> {
    let db_pool = get_db_pool().await;
    sqlx::query_as!(
        Todos,
        r#"
        select
            id,
            title,
            description,
            created_at
        from
            todos
        "#,
    )
    .fetch_all(&db_pool)
    .await
}

// ハンドラー
async fn get_todos() -> impl Responder {
    let todos = fetch_all().await;
    match todos {
        Ok(t) => HttpResponse::Ok().json(t),
        Err(e) => {
            println!("{e}");
            HttpResponse::NotFound().json("Not Found Error")
        }
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().route("/todos", web::get().to(get_todos)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

コードの実行

コードが全て揃ったので、cargo run を実行します。

cargo run

API実行クライアントツールを使うか、ブラウザに http://localhost:8080/todosと貼り付けるかすると、以下のような応答が返ってくることを確認できます!

API
全てのサンプルデータが取得できていることを確認できました。

おわりに

初めての試みで好奇心満載で実装することができました。
TiDBはパフォーマンス、スケーリング等に優れているという噂なので、高速な Web アプリケーションを作ることのできる Rust との組み合わせはかなり期待できそうです。

これを機に TiDB にも触れる機会を増やしていこうと思います!
では!

コラボスタイル Developers

Discussion