RustのDieselでPostgresに接続する with Docker

# はじめに
最近Rustの勉強をしていてAPI＋DB構成のアプリを作ろうとしています。
DBをPostgresにしてDieselから接続します。
DBの用意は[以前の記事](https://qiita.com/Yukimura127/items/c798cf9c3b19498ab8bf)で紹介した通りです。

Rustの環境は開発ということもあってホスト側に構築しようと思い、dieselをインストールしようとしたらエラーが...

```bash
$ cargo install diesel_cli --no-default-features --features postgres
error: linking with `link.exe` failed: exit code: 1181
・
・
・
error: aborting due to previous error

error: failed to compile `diesel_cli v1.4.1`, intermediate artifacts can be found at `C:\Users\Yukimura\AppData\Local\Temp\cargo-installTfifUZ`

Caused by:
  could not compile `diesel_cli`

To learn more, run the command again with --verbose.
```

調べてみると、libpqが足りないとかVC++のツールセットが足りないなど出てきました。
解決するのが面倒だったのでRustの環境もコンテナで作ることにしました。
コンテナ作成～RustのアプリケーションからPostgresに接続してCRUD操作をするところまでを備忘録として残します。

# Dockerfileをつくってコンテナ作成
```docker:Dockerfile
FROM rust:1.49

RUN cargo install diesel_cli --no-default-features --features postgres chrono network-address
CMD ["/bin/bash"]
```

ビルドして起動します。
Windowsなので%cd%でマウントします。

```bash
$ docker build -t rust-dev:latest -f Dockerfile .
$ docker run --name rust-dev --net host -v %cd%:/usr/src/application -it rust-dev
```

# DieselでDBの作成
コンテナに入るとdieselコマンドが既に使えるようになっています。

```bash
$ diesel -V
diesel 1.4.1
```

アプリケーションが格納されているディレクトリまで移動してDBの作成を行います。

```bash
$ cd /usr/src/application
$ DATABASE_URL=postgres://root:root@localhost/rust-dev > .env
$ diesel setup
Creating migrations directory at: /usr/src/application/migrations
Creating database: rust-dev
```

pgAgminでDBが出来ているのを確認します。
![pgAdmin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149028/3dc021dd-d9f1-a495-e579-3700050e137b.png)

# スキーマの作成
> diesel setup

した際に
migrationsディレクトリが作成され、その中に
> 00000000000000_diesel_initial_setup

というディレクトリができます。
このディレクトリの中のup.sql/down.sqlにはヘルパー機能等を設定するための記述がされています。
このup.sql/down.sqlに自前のスキーマを書くこともできるようですが、基本は別々に作るようです。

マシンを管理することを想定してスキーマを作成してみます。
マイグレーションします。

```bash
$ diesel migration generate create_machines
Creating migrations/2021-03-09-130738_create_machines/up.sql
Creating migrations/2021-03-09-130738_create_machines/down.sql
```

up.sqlとdown.sqlにSQLを書く。
CREATE TABLEを書いてテーブルの作成をするのがメインだが、マスタであればINSERTしたいケースもある。

```sql:up.sql
-- マシンステータステーブル
CREATE TABLE machine_statuses (
    id SERIAL PRIMARY KEY,
    name VARCHAR(20) NOT NULL
);
INSERT INTO machine_statuses (name) VALUES ('stop');
INSERT INTO machine_statuses (name) VALUES ('running');

-- マシンテーブル
CREATE TABLE machines (
  id SERIAL PRIMARY KEY,
  name VARCHAR(20) NOT NULL,
  mac_address MACADDR NOT NULL,
  status_id INTEGER NOT NULL,
  create_at timestamptz NOT NULL DEFAULT current_timestamp,
  update_at timestamptz NOT NULL DEFAULT current_timestamp,
  FOREIGN KEY (status_id) REFERENCES machine_statuses(id)
);
```

```sql:down.sql
DROP TABLE machines;
DROP TABLE machine_statuses;
```

DBに反映する。

```bash
$ diesel migration run
```

src/schema.rsが自動で生成される。

```rust:src/schema.rs
table! {
    machine_statuses (id) {
        id -> Int4,
        name -> Varchar,
    }
}

table! {
    machines (id) {
        id -> Int4,
        name -> Varchar,
        mac_address -> Macaddr,
        status_id -> Int4,
        create_at -> Timestamptz,
        update_at -> Timestamptz,
    }
}

joinable!(machines -> machine_statuses (status_id));

allow_tables_to_appear_in_same_query!(
    machine_statuses,
    machines,
);
```

pgAdminでも確認してみます。
![pgAdmin2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149028/47483bb4-9f5d-f2cd-ee6a-82f6a6cd49d8.png)


テーブルができてますね。
INSERTしたデータも入っています。
![pgAdmin3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149028/169d23a6-c7d3-b556-bc95-215558cefb85.png)


# CRUD操作
RustのプログラムからCRUD操作をしてみます。
まずは必要なライブラリを追加します。

```bash
$ cargo add diesel --no-default-features --features "postgres chrono network-address"
$ cargo add chrono
$ cargo add dotenv
$ cargo add hex-literal
```

```toml:Cargo.toml
[package]
name = "rust-dev"
version = "0.1.0"
authors = ["Yukimura"]
edition = "2018"

[dependencies]
chrono = "0.4.19"
diesel = { version = "1.4.6", features = ["postgres", "chrono", "network-address"], default-features = false }
dotenv = "0.15.0"
hex-literal = "0.3.1"
```

コネクションを作成する関数を定義します。

```rust:src/util.rs
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;

pub fn establish_connection() -> PgConnection {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    PgConnection::establish(&database_url)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}
```

モデルの構造体を定義します。
INSERTするための構造体も定義しています。

```rust:model.rs
use super::schema::machines;

#[derive(Debug, Queryable)]
pub struct MachineStatus {
    pub id: i32,
    pub name: String,
}

#[derive(Debug, Queryable)]
pub struct Machine {
    pub id: i32,
    pub name: String,
    pub mac_address: [u8; 6],
    pub status_id: i32,
    pub create_at: chrono::NaiveDateTime,
    pub update_at: chrono::NaiveDateTime,
}

#[derive(Insertable)]
#[table_name="machines"]
pub struct NewMachine {
    pub name: String,
    pub mac_address: [u8; 6],
    pub status_id: i32
}
```

ライブラリを管理するファイルを作成します。

```rust:lib.rs
#[macro_use]
extern crate diesel;

mod models;
mod schema;
mod utils;
```

CRUD操作を行う処理を作成します。

```rust:main.rs
#[macro_use]
extern crate diesel;

mod models;
mod schema;
mod utils;

use hex_literal;

use diesel::prelude::*;
use models::*;
use schema::machines as machines_schema;
use utils::establish_connection;

fn main() {
    // create connection
    let connection = establish_connection();

    // create
    let mac_address_a = hex_literal::hex!("52 42 00 1a a9 44");
    let mac_address_b = hex_literal::hex!("52 42 00 1a a9 45");
    let new_machine = vec![
        NewMachine {
            name: String::from("machineA"),
            mac_address: mac_address_a,
            status_id: 1
        },
        NewMachine {
            name: String::from("machineB"),
            mac_address: mac_address_b,
            status_id: 1
        },
    ];
    diesel::insert_into(machines_schema::dsl::machines)
       .values(&new_machine)
       .execute(&connection)
       .expect("Error creating machines");

    // read
    let machines = machines_schema::dsl::machines
        .load::<Machine>(&connection)
        .expect("Error loading machines");
    for machine in &machines {
        println!("{:?}", machine);
    }

    // update
    let update_machine = diesel::update(machines_schema::dsl::machines.find(machines[0].id))
        .set(machines_schema::name.eq("machineC"))
        .get_result::<Machine>(&connection)
        .expect("Error updating machines");
    println!("--------------------");
    println!("{:?}", update_machine);

    // delete
    diesel::delete(machines_schema::dsl::machines.find(machines[1].id))
        .execute(&connection)
        .expect("Error deleting machines");

    // select
    let machines = machines_schema::dsl::machines
        .load::<Machine>(&connection)
        .expect("Error loading machines");
    println!("--------------------");
    for machine in &machines {
        println!("{:?}", machine);
    }
}
```

まず
名前が「machineA」、MACアドレスが「52:42:00:1a:a9:44」というモデルと
名前と「machineB」、MACアドレスが「52:42:00:1a:a9:45」というモデルを追加しています。
そのあと全件読み込んで2件存在するか確認しています。
で、読み込んだ1件目の名前を「machineC」という名前に変えるように更新、読み込んだ2件目を削除しています。
再度全件読み込んで名前が変わっていること、1件になっていることを確認しています。

実行した結果が以下（MACアドレスは10進に変換されています）。

```bash
$ cargo run
Machine { id: 1, name: "machineA", mac_address: [82, 66, 0, 26, 169, 68], status_id: 1, create_at: 2021-03-18T14:01:48.632112, update_at: 2021-03-18T14:01:48.632112 }
Machine { id: 2, name: "machineB", mac_address: [82, 66, 0, 26, 169, 69], status_id: 1, create_at: 2021-03-18T14:01:48.632112, update_at: 2021-03-18T14:01:48.632112 }
--------------------
Machine { id: 1, name: "machineC", mac_address: [82, 66, 0, 26, 169, 68], status_id: 1, create_at: 2021-03-18T14:01:48.632112, update_at: 2021-03-18T14:01:48.632112 }
--------------------
Machine { id: 1, name: "machineC", mac_address: [82, 66, 0, 26, 169, 68], status_id: 1, create_at: 2021-03-18T14:01:48.632112, update_at: 2021-03-18T14:01:48.632112 }
```

pgAdmin上も以下のようになっています。あっていそうですね。
![pgAdmin4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149028/7bdf02bd-62ee-f117-3973-e19e97a3a2b7.png)

# まとめ
Dockerのコンテナ内でRustのDieselからPostgresに接続してCRUD操作する流れを書きました。

ORMマッパーってプログラムでモデルを定義してコマンドを打つとSQLが生成されて実行されるというものが多い印象です。
が、DieselではSQL文は自分で書く必要があります。
（面倒ですが、SQLは書けたほうがよいのでOKです）
CRUD操作はORMマッパーらしい操作でとても書きやすいですね。

次はAPIと絡めてアプリケーションとして作っていきたいと思います。