Skip to content

プロジェクトのセットアップ

環境準備

今回の演習は、(第一部)サーバーからデータベースを扱う の状態から開始します。

第一部から繰り返し受講している方も、 以下の手順でセットアップを行ってください。

  1. データベースを扱う準備 からプロジェクトをセットアップしましょう。

  2. .env ファイルを作成し、以下のように編集しましょう。

sh
DB_USERNAME="root"
DB_PASSWORD="password"
DB_HOSTNAME="localhost"
DB_PORT="3306"
DB_DATABASE="world"
  1. source .env を実行しましょう。

  2. 以下のコマンドを実行し、クレートの依存関係を追加しましょう。

sh
$ cargo add axum axum-extra anyhow serde serde_json tokio bcrypt --features tokio/full,serde/derive,axum/macros,axum-extra/typed-header
$ cargo add async-session tracing tracing-subscriber --features tracing-subscriber/env-filter,tracing-subscriber/fmt
$ cargo add tower-http --features add-extension,trace,fs

また、 cargo.toml に以下の記述を足しましょう。

toml
[dependencies.sqlx]
version = "0.7"
features = ["mysql", "migrate", "chrono", "runtime-tokio", "macros"] 

[dependencies.async-sqlx-session]
git = "https://github.com/maxcountryman/async-sqlx-session.git"
default-features = false
branch = "sqlx-0.7"
features = ["mysql"]

[dependencies] に既に sqlx がある場合は、その行を削除してください。

以上でセットアップはできているはずです。

ファイルの分割

このまま演習を始めてしまうとファイルが長くなりすぎてしまうので、ファイルを別のモジュールとして分割します。

TIP

パッケージとは、関連する複数のファイルをまとめる単位のことです。
詳しくは以下を参照してください。 The Rust Programming Language 日本語版 - パッケージとクレート

まずは、src ディレクトリにファイルを追加していきます。

この画像のようなディレクトリ構造を作成しましょう。 次に、それぞれのファイルにコードを追加していきます。

ファイルの内容

handler.rs

rs
use axum::{
    routing::{get, post},
    Router,
};

use crate::repository::Repository;

mod country;

pub fn make_router(app_state: Repository) -> Router {
    let city_router = Router::new()
        .route("/city/:city_name", get(country::get_city_handler))
        .route("/cities", post(country::post_city_handler));

    Router::new().nest("/", city_router).with_state(app_state)
}

main.rs

rs
use tower_http::trace::TraceLayer;
use tracing_subscriber::EnvFilter;

mod handler;
mod repository;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::try_from_default_env().unwrap_or("info".into()))
        .init();

    let app_state = repository::Repository::connect().await?;
    let app = handler::make_router(app_state).layer(TraceLayer::new_for_http());
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;

    tracing::info!("listening on {}", listener.local_addr()?);
    axum::serve(listener, app).await.unwrap();
    Ok(())
}

repository.rs

rs
use sqlx::mysql::MySqlConnectOptions;
use sqlx::mysql::MySqlPool;
use std::env;

pub mod country;

#[derive(Clone)]
pub struct Repository {
    pool: MySqlPool,
}

impl Repository {
    pub async fn connect() -> anyhow::Result<Self> {
        let options = get_options()?;
        let pool = sqlx::MySqlPool::connect_with(options).await?;
        Ok(Self {
            pool,
        })
    }
}

fn get_options() -> 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))
}

handler/country.rs

rs
use crate::repository::{country::City, Repository};
use axum::{
    extract::rejection::JsonRejection,
    extract::{Path, State},
    http::StatusCode,
    Json,
};

pub async fn get_city_handler(
    State(state): State<Repository>,
    Path(city_name): Path<String>,
) -> Result<Json<City>, StatusCode> {
    let city = Repository::get_city_by_name(&state, city_name).await;
    match city {
        Ok(city) => Ok(Json(city)),
        Err(sqlx::Error::RowNotFound) => Err(StatusCode::NOT_FOUND),
        Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
    }
}

pub async fn post_city_handler(
    State(state): State<Repository>,
    query: Result<Json<City>, JsonRejection>,
) -> Result<Json<City>, StatusCode> {
    match query {
        Ok(Json(city)) => {
            let result = Repository::create_city(&state, city).await;
            match result {
                Ok(city) => Ok(Json(city)),
                Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
            }
        }
        Err(_) => Err(StatusCode::BAD_REQUEST),
    }
}

repository/country.rs

rs
use super::Repository;

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

impl Repository {
    pub async fn get_city_by_name(&self, city_name: String) -> sqlx::Result<City> {
        sqlx::query_as::<_, City>("SELECT * FROM city WHERE Name = ?")
            .bind(&city_name)
            .fetch_one(&self.pool)
            .await
    }

    pub async fn create_city(&self, city: City) -> sqlx::Result<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(&self.pool)
        .await?;

        let id = result.last_insert_id() as i32;
        Ok(City {
            id: Some(id),
            ..city
        })
    }
}

変更点の説明

今まではmain.rsに全てのコードを記述しており、データベースやハンドラーの処理が混ざっていましたが、ここではそれらの処理をファイルに分割しました。

ルーティングの処理をhandler.rsに、データベースの初期化や接続をrepository.rsに分割しました。 handlerのサブモジュールとしてcountry.rsを作成し、都市に関する API の処理を記述しました。 同様にrepositoryのサブモジュールとしてcountry.rsを作成し、都市に関するデータベースの処理を記述しました。

準備完了

それでは、task up でデータベースを立ち上げてから cargo run で実行してみましょう。

localhost:8080/cities/Tokyoにアクセスして実際に動いていることを確認しましょう。

上手く動いていることを確認できたら、 Ctrl+C で一旦止めましょう。