Skip to content

サーバーからデータベースを扱う

axum を使い、データベースからデータを取得するサーバーアプリケーションを作りましょう。

rs
use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::get,
    Json, Router,
};
use sqlx::{mysql::MySqlConnectOptions, Pool};
use std::env;

#[derive(sqlx::FromRow, serde::Serialize)]
#[sqlx(rename_all = "PascalCase")]
#[serde(rename_all = "camelCase")]
struct City {
    #[sqlx(rename = "ID")]
    id: i32,
    name: String,
    country_code: String,
    district: String,
    population: i32,
}

fn get_option() -> anyhow::Result<MySqlConnectOptions> {
    let host = env::var("DB_HOSTNAME")?;
    let port = env::var("DB_PORT")?.parse()?;
    let username = env::var("DB_USERNAME")?;
    let password = env::var("DB_PASSWORD")?;
    let database = env::var("DB_DATABASE")?;
    let timezone = Some(String::from("Asia/Tokyo"));
    let collation = String::from("utf8mb4_unicode_ci");

    Ok(MySqlConnectOptions::new()
        .host(&host)
        .port(port)
        .username(&username)
        .password(&password)
        .database(&database)
        .timezone(timezone)
        .collation(&collation))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let options = get_option()?;
    let pool = sqlx::MySqlPool::connect_with(options).await?;

    let app = Router::new()
        .route("/cities/:cityName", get(get_city_handler))
        .with_state(pool);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
        .await
        .unwrap();

    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();

    Ok(())
}

async fn get_city_handler(
    State(pool): State<Pool<sqlx::MySql>>,
    Path(city_name): Path<String>,
) -> Result<Json<City>, StatusCode> {
    let city = sqlx::query_as::<_, City>("SELECT * FROM city WHERE Name = ?")
        .bind(&city_name)
        .fetch_one(&pool)
        .await;

    match city {
        Ok(city) => Ok(Json(city)),
        Err(sqlx::Error::RowNotFound) => Err(StatusCode::NOT_FOUND),
        Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
    }
}

都市が見つかったら200を、見つからなかったら404を返しています。 Postman からリクエストを送ってみましょう。

画像のように返ってきたら成功です。

自分が好きな都市の情報を取得する API を叩いて、そのレスポンスのスクリーンショットを講習会チャンネルに投稿しましょう。

基本問題

都市を追加する API を追加してみましょう。

ヒント 1

都市を追加するということはクライアントから情報を受け取る必要があります。このようなときは、どのメソッドを使えばいいでしょうか。

ヒント 2

メソッドはPOSTを使いましょう。リクエストボディには JSON を使いましょう。どのようにすれば JSON を扱えたでしょうか。

答え
rs
use axum::{
    extract::{rejection::JsonRejection, Path, State},
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use sqlx::{mysql::MySqlConnectOptions, Pool};
use std::env;

#[derive(sqlx::FromRow, serde::Serialize, serde::Deserialize)] 
#[sqlx(rename_all = "PascalCase")]
#[serde(rename_all = "camelCase")]
struct City {
    #[sqlx(rename = "ID")]
    id: Option<i32>, 
    name: String,
    country_code: String,
    district: String,
    population: i32,
}

fn get_option() -> anyhow::Result<MySqlConnectOptions> {
    let host = env::var("DB_HOSTNAME")?;
    let port = env::var("DB_PORT")?.parse()?;
    let username = env::var("DB_USERNAME")?;
    let password = env::var("DB_PASSWORD")?;
    let database = env::var("DB_DATABASE")?;
    let timezone = Some(String::from("Asia/Tokyo"));
    let collation = String::from("utf8mb4_unicode_ci");

    Ok(MySqlConnectOptions::new()
        .host(&host)
        .port(port)
        .username(&username)
        .password(&password)
        .database(&database)
        .timezone(timezone)
        .collation(&collation))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let options = get_option()?;
    let pool = sqlx::MySqlPool::connect_with(options).await?;

    let app = Router::new()
        .route("/cities/:cityName", get(get_city_handler)) 
        .route("/cities", post(post_city_handler)) 
        .with_state(pool);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
        .await
        .unwrap();

    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();

    Ok(())
}

async fn get_city_handler(
    State(pool): State<Pool<sqlx::MySql>>,
    Path(city_name): Path<String>,
) -> Result<Json<City>, StatusCode> {
    let city = sqlx::query_as::<_, City>("SELECT * FROM city WHERE Name = ?")
        .bind(&city_name)
        .fetch_one(&pool)
        .await;

    match city {
        Ok(city) => Ok(Json(city)),
        Err(sqlx::Error::RowNotFound) => Err(StatusCode::NOT_FOUND),
        Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
    }
}

async fn post_city_handler( 
    State(pool): State<Pool<sqlx::MySql>>, 
    query: Result<Json<City>, JsonRejection>, 
) -> Result<Json<City>, StatusCode> { 
    match query { 
        Ok(Json(mut city)) => {     
            let result = sqlx::query( 
                "INSERT INTO city (Name, CountryCode, District, Population) VALUES (?, ?, ?, ?)", 
            ) 
            .bind(&city.name) 
            .bind(&city.country_code) 
            .bind(&city.district) 
            .bind(city.population) 
            .execute(&pool) 
            .await
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 

            city.id = Some(result.last_insert_id() as i32); 
            Ok(Json(city)) 
        } 
        Err(_) => Err(StatusCode::BAD_REQUEST), 
    }   
}  

架空の都市や実在する都市を Postman から追加して、レスポンスのスクリーンショットを講習会チャンネルに投稿しましょう。

応用問題

さまざまな API を作ってみましょう。

    • 国の情報を取得する
    • 都市をすべて取得する
    • 既にある都市や国の情報を変更する