diff --git a/docs/chapter1/section4/3_rust_and_db.md b/docs/chapter1/section4/3_rust_and_db.md index c28121a9..b6a546c8 100644 --- a/docs/chapter1/section4/3_rust_and_db.md +++ b/docs/chapter1/section4/3_rust_and_db.md @@ -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` により、環境変数を読み込んでいます。環境変数を使うことで、プログラムの動作を変えることなく、データベースの接続情報を変更できます。 ### 環境変数を設定する @@ -45,10 +36,9 @@ export DB_DATABASE="world" ```txt ... -# Go workspace file -go.work +# Added by cargo -.tool-versions +/target .env // [!code ++] ``` @@ -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 つのレコードを取得しています。 ### 基本問題 @@ -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 @@ -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 文は下のようになります。 diff --git a/docs/chapter1/section4/src/practice_advanced.go b/docs/chapter1/section4/src/practice_advanced.go deleted file mode 100644 index 8fcf9ad9..00000000 --- a/docs/chapter1/section4/src/practice_advanced.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "database/sql" - "errors" - "log" - "os" - "time" - - "github.com/go-sql-driver/mysql" - "github.com/jmoiron/sqlx" -) - -type City struct { - ID int `json:"ID,omitempty" db:"ID"` - Name string `json:"name,omitempty" db:"Name"` - CountryCode string `json:"countryCode,omitempty" db:"CountryCode"` - District string `json:"district,omitempty" db:"District"` - Population int `json:"population,omitempty" db:"Population"` -} - -func main() { - jst, err := time.LoadLocation("Asia/Tokyo") - if err != nil { - log.Fatal(err) - } - - conf := mysql.Config{ - User: os.Getenv("DB_USERNAME"), - Passwd: os.Getenv("DB_PASSWORD"), - Net: "tcp", - Addr: os.Getenv("DB_HOSTNAME") + ":" + os.Getenv("DB_PORT"), - DBName: os.Getenv("DB_DATABASE"), - ParseTime: true, - Collation: "utf8mb4_unicode_ci", - Loc: jst, - } - - db, err := sqlx.Open("mysql", conf.FormatDSN()) - - if err != nil { - log.Fatal(err) - } - - log.Println("connected") - - cityName := os.Args[1] - - var city City - err = db.Get(&city, "SELECT * FROM city WHERE Name = ?", cityName) - if errors.Is(err, sql.ErrNoRows) { - log.Printf("no such city Name = '%s'\n", cityName) - return - } - if err != nil { - log.Fatalf("DB Error: %s\n", err) - } - - log.Printf("%sの人口は%d人です\n", city.Name, city.Population) - - var population int //[!code ++] - err = db.Get(&population, "SELECT Population FROM country WHERE Code = ?", city.CountryCode) //[!code ++] - if errors.Is(err, sql.ErrNoRows) { //[!code ++] - log.Printf("no such country Code = '%s'\n", city.CountryCode) //[!code ++] - return //[!code ++] - } //[!code ++] - if err != nil { //[!code ++] - log.Fatalf("DB Error: %s\n", err) //[!code ++] - } //[!code ++] - //[!code ++] - percent := (float64(city.Population) / float64(population)) * 100 //[!code ++] - //[!code ++] - log.Printf("これは%sの人口の%f%%です\n", city.CountryCode, percent) //[!code ++] -} diff --git a/docs/chapter1/section4/src/practice_advanced.rs b/docs/chapter1/section4/src/practice_advanced.rs new file mode 100644 index 00000000..0c9da70a --- /dev/null +++ b/docs/chapter1/section4/src/practice_advanced.rs @@ -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 { + 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(()) +} diff --git a/docs/chapter1/section4/src/select.go b/docs/chapter1/section4/src/select.go deleted file mode 100644 index ae165400..00000000 --- a/docs/chapter1/section4/src/select.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "log" - "os" - "time" - - "github.com/go-sql-driver/mysql" - "github.com/jmoiron/sqlx" -) - -type City struct { - ID int `json:"ID,omitempty" db:"ID"` - Name string `json:"name,omitempty" db:"Name"` - CountryCode string `json:"countryCode,omitempty" db:"CountryCode"` - District string `json:"district,omitempty" db:"District"` - Population int `json:"population,omitempty" db:"Population"` -} - -// #region main -func main() { - jst, err := time.LoadLocation("Asia/Tokyo") - if err != nil { - log.Fatal(err) - } - - conf := mysql.Config{ - User: os.Getenv("DB_USERNAME"), - Passwd: os.Getenv("DB_PASSWORD"), - Net: "tcp", - Addr: os.Getenv("DB_HOSTNAME") + ":" + os.Getenv("DB_PORT"), - DBName: os.Getenv("DB_DATABASE"), - ParseTime: true, - Collation: "utf8mb4_unicode_ci", - Loc: jst, - } - - db, err := sqlx.Open("mysql", conf.FormatDSN()) - - if err != nil { - log.Fatal(err) - } - - log.Println("connected") - - var cities []City - err = db.Select(&cities, "SELECT * FROM city WHERE CountryCode = 'JPN'") //?を使わない場合、第3引数以降は不要 - if err != nil { - log.Fatal(err) - } - - log.Println("日本の都市一覧") - for _, city := range cities { - log.Printf("都市名: %s, 人口: %d\n", city.Name, city.Population) - } -} - -//#endregion main diff --git a/docs/chapter1/section4/src/select.rs b/docs/chapter1/section4/src/select.rs new file mode 100644 index 00000000..276fb20a --- /dev/null +++ b/docs/chapter1/section4/src/select.rs @@ -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 { + 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