【Rust】 Rust + PostgreSQL + tokio_postgresでDBアクセスする方法

:::note warn 
ゆるふわです
:::

#  やりたいこと
**Rust**で簡単にサクッとローカルの**PostgreSQL**と連携し、こんな感じのテーブルを**Rust側から操作**して作ります

```rust:これを作るよ
     id      |   name   
-------------+----------
  -838073477 | John Doe
 -1966422589 | John Doe
 -2115227795 | John Doe
(3 rows)
```


:::note warn
`id` や `name`には特に深い意味はありません
サンプルとして適当な値を入れているだけです
:::

Rust側では標準入力で

|標準入力|機能|
|-------|----|
|a|乱数のidと名前(今回はJohn Doe固定)をrecordというdbに追加|
|s|テーブルを全表示|
|d|テーブルを全削除|


といった機能をつけました。

## 動作環境
|項目|値|
|-------|----|
|OS|Ubuntu22.04 On WSL2|
|Rust|1.70.0|
|psql (PostgreSQL)|15.3 |


# 流れ
1. postgreSQLでrecordという名前の空のDBを作成
2. Rustでプログラムを作成
3. a, s, d等を入力してRust側でrecordを操作
4. PostgreSQL側でテーブルを確認

といった手順になります。
それではそれぞれの項目について述べます。

## 1. postgreSQLで空のDB(名:record)を作成
### 1．インストール等の事前準備
```
sudo apt update
sudo apt install postgresql
sudo systemctl status postgresql
```

### 2\. ターミナルを開いて`postgres`ユーザーに切り替え
```
sudo -u postgres psql
```

### 3\. アカウントと`record`DBの作成 
```
CREATE USER your_username WITH PASSWORD 'your_password';
CREATE DATABASE record;
```
:::note info
`your_username`, `your_password`を自分で設定してください
DB名を変えたい場合は、recordを任意の名前に置き換えてください
:::

### 4\. ユーザー"your_username"に対して権限付与
```
GRANT ALL PRIVILEGES ON DATABASE type_record TO your_username;
GRANT ALL PRIVILEGES ON SCHEMA public TO your_username;
```
データベースとスキーマ、両方にアクセス権限を付与することに注意してください
(私はここでDBにしか与えておらず、後に2時間ほど詰まりました)

## 2. Rustでプログラムを作成
### 1. dependenciesにクレートを追加
```diff:Cargo.toml
[dependencies]
+ tokio-postgres = "0.7"
+ tokio = { version = "1", features = ["full"] }
+ dotenv = "0.15.0"
+ rand = "0.8.4"
```
### 2. .envファイルに追記

```diff:.env
+ DB_HOST=localhost
+ DB_PORT=5432
+ DB_USER=your_username
+ DB_PASSWORD=your_password
+ DB_NAME=record
```
:::note info
`your_username`, `your_password`は
[3.アカウントと`record`DBの作成](###3\.アカウントと`record`DBの作成)
で作成したものと同じです
:::
.envファイルが無い方は、rustで作業しているディレクトリのルートに戻って
```
code .env
```
等で作成してください

## 3. Rustで実装

以下に例を示します。
適宜コメントを入れました。

```rust:main.rs
use dotenv::dotenv;
use std::env;
use std::io::{self, BufRead};
use tokio_postgres::{NoTls, Error};
use rand::Rng;

#[tokio::main]
async fn main() -> Result<(), Error> {
    // .env ファイルを読み込み
    dotenv().ok();

    // 環境変数から接続情報を取得
    let db_host = env::var("DB_HOST").expect("DB_HOST not set");
    let db_port = env::var("DB_PORT").expect("DB_PORT not set");
    let db_user = env::var("DB_USER").expect("DB_USER not set");
    let db_password = env::var("DB_PASSWORD").expect("DB_PASSWORD not set");
    let db_name = env::var("DB_NAME").expect("DB_NAME not set");

    // PostgreSQLの接続情報を設定
    let connection_string = format!(
        "host={} port={} user={} password={} dbname={}",
        db_host, db_port, db_user, db_password, db_name
    );

    let (client, connection) = tokio_postgres::connect(&connection_string, NoTls).await?;

    // 接続タスクをスポーンして実行
    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });

    // テーブル作成のクエリを実行
    client
        .batch_execute(
            "
            CREATE TABLE IF NOT EXISTS users (
                id SERIAL PRIMARY KEY,
                name TEXT NOT NULL
            )
            ",
        )
        .await?;

    // 標準入力からキー入力を受け取るためのストリームを作成
    let stdin = io::stdin();
    let mut input_stream = stdin.lock().lines();

    println!("Press 'a' to add a record, 'd' to delete all records, 's' to sort and display records, or any other key to exit.");

    // キー入力を監視し、対応する操作を実行
    while let Some(Ok(input)) = input_stream.next() {
        match input.as_str() {
            "a" => {
                // レコードの追加
                let mut rng = rand::thread_rng();
                let id: i32 = rng.gen();

                client
                    .execute(
                        "INSERT INTO users (id, name) VALUES ($1, $2)",
                        &[&id, &"John Doe"],
                    )
                    .await?;
                println!("Record added.");
            }
            "d" => {
                // レコードの削除
                client.execute("DELETE FROM users", &[]).await?;
                println!("All records deleted.");
            }
            "s" => {
                // レコードのソートと表示
                let rows = client
                    .query("SELECT id, name FROM users ORDER BY id", &[])
                    .await?;
                for row in rows {
                    let id: i32 = row.get(0);
                    let name: &str = row.get(1);
                    println!("ID: {}, Name: {}", id, name);
                }
            }
            _ => {
                break;
            }
        }
    }

    Ok(())
}
```

# 3. [a, s, d]等を入力してRust側でrecordを操作
```
cargo run
```
で
```
Press 'a' to add a record, 'd' to delete all records, 's' to sort and display records, or any other key to exit.
```
などと表示されるので、[a]や[d], [s]をポチポチして`record`を操作していきましょう。

```rust:実行例
Press 'a' to add a record, 'd' to delete all records, 's' to sort and display records, or any other key to exit.
a
Record added.
a
Record added.
a
Record added.
s
ID: -2115227795, Name: John Doe
ID: -1966422589, Name: John Doe
ID: -838073477, Name: John Doe
```
正しくテーブルを作成することができました。
地味にIDでソートされています。

# 4. postgreSQL側でテーブルを確認
```
sudo -u postgres 
psql -d type_record -c "SELECT * FROM users;"
```
を実行します。
以下のような結果が得られると成功です。

```rust:実行例
postgres=# \c type_record
You are now connected to database "type_record" as user "postgres".
type_record=#  SELECT * FROM users;
     id      |   name   
-------------+----------
  -838073477 | John Doe
 -1966422589 | John Doe
 -2115227795 | John Doe
(3 rows)
```

## おわりに
Rust側からローカルのPostgreSQLをいじることができました。
HerokuやRender.com上のPostgreSQLとも連携できれば、Webアプリ開発の幅が広がりそうです。
お疲れさまでした。