Rust | Actix Web と slqx を使って TiDB Serverless にアクセスする
はじめに
今更ながら、ゼルダの時のオカリナにハマっている Shota ですw
今回は、Rust の webフレームワークの Actix Web ✖︎ sqlx ✖︎ TiDB で簡易的なAPIサーバーを構築しましたので、紹介をしていきます!
他の参考記事もほとんどなかったので、ぜひ本記事が参考になれば嬉しいです。
Actix Web については以前に公開した記事がありますので、こちらも参考になればと。
🔻Actix Web
🔻sqlx
🔻TiDB
下準備
プロジェクトの作成
以下でプロジェクトを作成します。
cargo new my-app
cd my-app
プロジェクトフォルダ直下の、Cargo.toml に以下のクレート群を追加しておきます。
TiDB は MySQL 互換ですので、sqlx では MySQL のオプションを使いながら接続をします。
[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を定義しておきます。
DATABASE_URL=mysql://{ユーザー名}.root:{パスワード}@gateway01.ap-northeast-1.prod.aws.tidbcloud.com:4000/test
テーブルとサンプルデータの作成
TiDB のクラスターページにアクセスし、左タブの「SQL Editor」で、TODO のテーブルとサンプルデータを詰め込んだ SQL を実行していきます。
実行する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
}
最終的には以下のようなコードになります。
最終的な全てのコード
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
と貼り付けるかすると、以下のような応答が返ってくることを確認できます!
全てのサンプルデータが取得できていることを確認できました。
おわりに
初めての試みで好奇心満載で実装することができました。
TiDBはパフォーマンス、スケーリング等に優れているという噂なので、高速な Web アプリケーションを作ることのできる Rust との組み合わせはかなり期待できそうです。
これを機に TiDB にも触れる機会を増やしていこうと思います!
では!
Discussion