Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第一部 - サーバーからデータベースを扱う #37

Merged
merged 1 commit into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/chapter1/section4/3_rust_and_db.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ $ source .env
このコマンドによって読み込んだ環境変数は、コマンドを入力したターミナルを終了すると消えてしまいます。また、コマンドを入力したターミナル以外では環境変数として読み込まれません。新しくターミナルを開きなおした場合などは、もう一度実行してください。
:::

### クレートの依存関係を追加する
```sh
$ cargo add axum anyhow serde serde_json tokio --features tokio/full,serde/derive
$ cargo add sqlx --features mysql,migrate,chrono,runtime-tokio
```

### 実行する

```sh
Expand Down
10 changes: 2 additions & 8 deletions docs/chapter1/section4/4_server_and_db.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

<<< @/chapter1/section4/src/server.go{go:line-numbers}
<<< @/chapter1/section4/src/server.rs{rs:line-numbers}

都市が見つかったら`200`を、見つからなかったら`404`を返しています。
Postman からリクエストを送ってみましょう。
Expand All @@ -26,13 +26,7 @@ Postman からリクエストを送ってみましょう。

:::details 答え

- `main`関数内部

<<< @/chapter1/section4/src/practice_server.go#echo

- `postCityHandler`関数を定義

<<< @/chapter1/section4/src/practice_server.go#func
<<< @/chapter1/section4/src/practice_server.rs{rs:line-numbers}

:::

Expand Down
98 changes: 0 additions & 98 deletions docs/chapter1/section4/src/practice_server.go

This file was deleted.

99 changes: 99 additions & 0 deletions docs/chapter1/section4/src/practice_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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)] // [!code warning]
#[sqlx(rename_all = "PascalCase")]
#[serde(rename_all = "camelCase")]
struct City {
#[sqlx(rename = "ID")]
id: Option<i32>, // [!code warning]
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)) // [!code ++]
.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( // [!code ++]
State(pool): State<Pool<sqlx::MySql>>, // [!code ++]
query: Result<Json<City>, JsonRejection>, // [!code ++]
) -> Result<Json<City>, StatusCode> { // [!code ++]
match query { // [!code ++]
Ok(Json(mut city)) => { // [!code ++]
let result = sqlx::query( // [!code ++]
"INSERT INTO city (Name, CountryCode, District, Population) VALUES (?, ?, ?, ?)", // [!code ++]
) // [!code ++]
.bind(&city.name) // [!code ++]
.bind(&city.country_code) // [!code ++]
.bind(&city.district) // [!code ++]
.bind(city.population) // [!code ++]
.execute(&pool) // [!code ++]
.await // [!code ++]
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // [!code ++]

city.id = Some(result.last_insert_id() as i32); // [!code ++]
Ok(Json(city)) // [!code ++]
} // [!code ++]
Err(_) => Err(StatusCode::BAD_REQUEST), // [!code ++]
} // [!code ++]
} // [!code ++]
76 changes: 0 additions & 76 deletions docs/chapter1/section4/src/server.go

This file was deleted.

74 changes: 74 additions & 0 deletions docs/chapter1/section4/src/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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),
}
}
Loading