Skip to content

Commit

Permalink
add advanced and selecet
Browse files Browse the repository at this point in the history
  • Loading branch information
kenken714 committed Oct 16, 2024
1 parent d181e8f commit bec670a
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 161 deletions.
55 changes: 26 additions & 29 deletions docs/chapter1/section4/3_rust_and_db.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,8 @@ Rust でデータベースに接続するためのライブラリは様々あり

<<< @/chapter1/section4/src/connect_db.rs{rust:line-numbers}


書き換えた後、 import の周りで赤字のエラーが出た場合は、ターミナルで`go mod tidy`を実行してください。
26 から 40 行目でデータベースに接続するための設定をして、42 行目の`db, err := sqlx.Open("mysql", conf.FormatDSN())`でデータベースに接続しています。32 行目などで`os.Getenv()`という関数が出てきていますが、これは環境変数と呼ばれる、コンピューター側で設定してプログラムで使えるようにしている変数です。今は必要なデータベースのパスワードなどの環境変数を何も設定していないので、設定します。

:::info 詳しく知りたい人向け
**dsn とは**

42 行目に`FormatDSN`という関数がありますが、`DSN`は「**D**ata **S**ource **N**ame」の頭文字をとったものです。プログラムがデータベースを指定するために使われます。今回の`FormatDSN`という関数は、データベースのユーザー名、パスワード、使うデータベース、どこにデータベースのサーバーがあるのか、用いる標準時、文字種などの設定を`conf`という変数から読み取って DSN を組み立てています。

[Wikipedia DSN(英語)](https://en.wikipedia.org/wiki/Data_source_name)
:::
`get_option` 関数により、データベースに接続するための設定を構成し、`MySqlPool::connect_with` でデータベースに接続しています。
`env::var` により、環境変数を読み込んでいます。環境変数を使うことで、プログラムの動作を変えることなく、データベースの接続情報を変更できます。

### 環境変数を設定する

Expand All @@ -45,10 +36,9 @@ export DB_DATABASE="world"

```txt
...
# Go workspace file
go.work
# Added by cargo
.tool-versions
/target
.env // [!code ++]
```

Expand Down Expand Up @@ -84,13 +74,12 @@ Tokyoの人口は7980230人です

<<< @/chapter1/section4/src/connect_db.rs#city

この`City`構造体の横にあるバッククオートで囲まれたタグに`db`でデータベースのカラム名を指定します。これによってライブラリがデータベースから取得したレコードを構造体に上手くあてはめてくれます。

参考: [Struct タグについて | text.Baldanders.info](https://text.baldanders.info/golang/struct-tag/)
`#[derive(sqlx::FromRow)]`を使うことで、SQL 文で取得したレコードを構造体へ変換できるようになります。`#[sqlx(rename_all = "PascalCase")]` によって、データベースのカラム名が`PascalCase`に変換されます。また、`#[sqlx(rename = "ID")]` によって、`ID`というカラム名を`id`というフィールドに変換しています。

<<< @/chapter1/section4/src/connect_db.rs#get

`City`型の`city`という変数のポインタを sqlx ライブラリの`Get`関数の第 1 引数に指定します。第 2 引数には SQL 文を書きます。`Name = ?`としていますが、第 3 引数以降の値が順番に`?`へと当てはめられて SQL 文が実行され、取得したレコードが`city`変数に代入されます。
`sqlx::query_as` により、SQL 文を実行して結果を構造体に変換しています。SQL 文中の `?` に対して、`bind` で値を順番に結び付けることができます。
`fetch_one` により 1 つのレコードを取得しています。

### 基本問題

Expand All @@ -113,14 +102,14 @@ $ cargo run {都市の名前}
ヒント: 1 回のクエリでも取得できますが、2 回に分けた方が楽に考えられます。

:::details 答え
<<< @/chapter1/section4/src/practice_advanced.go
<<< @/chapter1/section4/src/practice_advanced.rs
:::

## 複数レコードを取得する

`Get`関数の代わりに`Select`関数を使い、第 1 引数を配列のポインタに変えると、複数レコードを取得できます。`main.go``main`関数を以下のように書き換えて実行してみましょう。
`fetch_one`関数の代わりに`fetch_all`関数を使い、第 1 引数を配列のポインタに変えると、複数レコードを取得できます。`main.rs``main`関数を以下のように書き換えて実行してみましょう。

<<< @/chapter1/section4/src/select.go#main{27 go:line-numbers}
<<< @/chapter1/section4/src/select.rs#main{rs:line-numbers}
以下のように日本の都市一覧を取得できます。

```txt
Expand All @@ -139,21 +128,29 @@ connected

## レコードを書き換える

`INSERT``UPDATE``DELETE`を実行したい場合は、`Exec`関数を使うことができます。第 1 引数に SQL 文を渡し、第 2 引数以降は`?`に当てはめたい値を入れます
`INSERT``UPDATE``DELETE`を実行したい場合は、`query`関数を使うことができます。

```go
result, err := db.Exec("INSERT INTO city (Name, CountryCode, District, Population) VALUES (?,?,?,?)", name, countryCode, district, population)
```rs
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?;
```

例えば`INSERT`ならば、このように使うことができます。`result`には操作によって変更があったレコード数などの情報が入っています
例えば`INSERT`ならば、このように使うことができます。return で返ってくる`result`には、`INSERT`で何件のレコードが追加されたかなどの情報が入っています

:::info 詳しく知りたい人向け
**なぜ`?`」を使うのか**
**なぜSQL文で`?`」を使うのか**

sqlx で変数を含む SQL を使いたいときは「`?`」を使わなくてはいけません。これはセキュリティ上の問題です。例として、国のコードからその国の都市の情報一覧を取得することを考えましょう。`fmt`ライブラリの`Sprintf`関数を使うとこのように処理を書くことができます
sqlx で変数を含む SQL を使いたいときは「`?`」を使わなくてはいけません。これはセキュリティ上の問題です。例として、国のコードからその国の都市の情報一覧を取得することを考えましょう。`format!`を使って SQL 文を作成すると以下のようになります

```go
err = db.Select(&city, fmt.Sprintf("SELECT * FROM city WHERE CountryCode = '%s'", code))
```rs
sqlx::query_as::<_, City>(
format!("SELECT * FROM city WHERE CountryCode = '{}'", code).as_str(),
)
```

`code`に入っている値がただの国名コードなら問題はないのですが、`JPN' OR 'A' = 'A`という値が入っていたらどうなるでしょうか。データベースで実行されるとき、SQL 文は下のようになります。
Expand Down
74 changes: 0 additions & 74 deletions docs/chapter1/section4/src/practice_advanced.go

This file was deleted.

68 changes: 68 additions & 0 deletions docs/chapter1/section4/src/practice_advanced.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use anyhow::Ok;
use sqlx::mysql::MySqlConnectOptions;
use std::env;

#[derive(sqlx::FromRow)]
#[sqlx(rename_all = "PascalCase")]
#[allow(dead_code)] // 使用していないフィールドへの警告を抑制
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 city_name = env::args().nth(1).expect("city name is required");
println!("Connected");
let city = sqlx::query_as::<_, City>("SELECT * FROM city WHERE Name = ?")
.bind(&city_name)
.fetch_one(&pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => anyhow::anyhow!("no such city Name = {}\n", &city_name),
_ => anyhow::anyhow!("DB error: {}", e),
})?;

println!("{}の人口は{}人です", &city.name, &city.population);

let population: i64 = sqlx::query_scalar("SELECT Population FROM country WHERE Code = ?") // [!code ++]
.bind(&city.country_code) // [!code ++]
.fetch_one(&pool) // [!code ++]
.await // [!code ++]
.map_err(|e| match e { // [!code ++]
sqlx::Error::RowNotFound => { // [!code ++]
anyhow::anyhow!("no such country Code = {}\n", &city.country_code) // [!code ++]
} // [!code ++]
_ => anyhow::anyhow!("DB error: {}", e), // [!code ++]
})?; // [!code ++]
let percent = city.population as f64 / population as f64 * 100.0; // [!code ++]
println!("これは、{}の人口の{:.2}%です", &city.country_code, percent); // [!code ++]

Ok(())
}
58 changes: 0 additions & 58 deletions docs/chapter1/section4/src/select.go

This file was deleted.

55 changes: 55 additions & 0 deletions docs/chapter1/section4/src/select.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use anyhow::Ok;
use sqlx::mysql::MySqlConnectOptions;
use std::env;

#[derive(sqlx::FromRow)]
#[sqlx(rename_all = "PascalCase")]
#[allow(dead_code)] // 使用していないフィールドへの警告を抑制
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))
}

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

println!("Connected");

let cities = sqlx::query_as::<_, City>("SELECT * FROM city WHERE CountryCode = ?")
.bind("JPN")
.fetch_all(&pool)
.await?;

println!("日本の都市一覧");
for city in cities {
println!("都市名: {}, 人口: {}", city.name, city.population);
}
Ok(())
}
// #endregion main

0 comments on commit bec670a

Please sign in to comment.