RustのORMライブラリdieselのチュートリアルをやってみた！

この記事は「[【マイスター・ギルド】本物のAdvent Calendar 2022](https://adventar.org/calendars/7794)」7日目の記事です。

## はじめに
Rustのアウトプットとして、[dieselのチュートリアル](https://diesel.rs/guides/getting-started)を使ってCRUD機能を試してみました。
全てチュートリアルレベルなので、初学者向きです。

## 本編

### 環境構築
docker-composeを使用しました。
docker単体で構築する場合は、コンテナ間network接続時にservice名を使用できないため名前付きネットワークを定義してIPを固定したりする必要があるかもしれないです。

```docker-compose.yaml
services:
  app:
    build:
      context: ./diesel_tutorial
    user: 1000:1000
    volumes:
      - ./diesel_tutorial:/home/yuhei3017/diesel_tutorial
      - /etc/passwd:/etc/passwd:ro
      - /etc/group:/etc/group:ro
    tty: true
    expose:
      - 8080
    depends_on:
      - db
  db:
    image: mysql:8-debian
    volumes:
      - ./db/data:/var/lib/mysql
      - ./db/config/my.cnf:/etc/mysql/my.cnf
    environment:
      - MYSQL_ROOT_PASSWORD=root123
    expose:
      - 3306
```

```Dockerfile
FROM rust:1.65-buster

WORKDIR /home/yuhei3017/diesel_tutorial
COPY . .

ENTRYPOINT ["bash"]
```

まずは、新しいプロジェクトを開始します。
[Cargo](https://doc.rust-lang.org/cargo/index.html)とはRustのパッケージマネジャ兼ビルドツールです。
```bash
$ cargo new --lib diesel_tutorial
```

最終的なファイル構成も載せておきます。
```bash
.
├── compose.yaml
├── db
│   └── config
│       └── my.cnf
└── diesel_tutorial
    ├── Cargo.lock
    ├── Cargo.toml
    ├── Dockerfile
    ├── diesel.toml
    ├── migrations
    │   └── 2022-11-27-034622_create_tasks
    │       ├── down.sql
    │       └── up.sql
    └── src
        ├── bin
        │   ├── add_task.rs
        │   ├── clean_task.rs
        │   ├── show_tasks.rs
        │   └── switch_task.rs
        ├── lib.rs
        ├── models.rs
        └── schema.rs
```

#### diesel

dieselをインストールするためCargo.tomlへ記述しておきます。
今回はmysqlへ接続してみようと思います。
こうしておけばビルド時にCargoがクレートのソースコードをダウンロードしてくれます。
(クレートとは、1つのライブラリもしくはいくつかの実行ファイル諸々がまとまった単位です)
```Cargo.toml
[dependencies]
diesel = { version = "2.0.0", features = ["mysql"] }
dotenvy = "0.15"
```
続いて[diesel_cli](https://github.com/diesel-rs/diesel/tree/master/diesel_cli)のインストールです。
diesel_cliは必須ではありませんが
- マイグレーションファイルの実行
- データベーススキーマを表すrustファイルの自動作成
をサポートしています。
```bash
cargo install diesel_cli
```

### 実装
#### データベース接続

<!-- mysqlのバージョンが異なるため、以下のようなエラーが出ます。
```bash
Plugin sha256_password could not be loaded: /usr/lib/x86_64-linux-gnu/mariadb19/plugin/sha256_password.so: cannot open shared object file: No such file or directory
```
安直ですが、旧バージョンへ合わせるようにしました。
```mysql
select user, host, plugin from mysql.user;
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root123';
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root123';
``` -->

dieselのチュートリアルに則って、
環境変数ファイルを作成します。

```bash
echo DATABASE_URL=mysql://root:root123@db:3306/diesel_tutorial > .env
```

`diesel setup`によって、
appコンテナからdbコンテナのmysqlへ接続し、データベースが作成されます。
また、カレントへmigrationディレクトリが作成されます。
```bash
root@49fd6e0fd870:/home/yuhei3017/diesel_tutorial# diesel setup
Creating database: diesel_tutorial
```
<!-- この時すでにマイグレーションファイルが存在する場合は勝手にmigration runしてくれるらしい -->

#### マイグレーションファイル、テーブル作成

以下のコマンドによって、マイグレーションファイルが作成されます。
```bash
$ diesel migration generate create_tasks
Creating migrations/2022-11-27-034622_create_tasks/up.sql
Creating migrations/2022-11-27-034622_create_tasks/down.sql
```

up.sqlとdown.sqlを編集します。

```sql:up.sql
-- Your SQL goes here
CREATE TABLE tasks (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title CHAR(20) NOT NULL,
  body TEXT NOT NULL,
  done BOOLEAN NOT NULL DEFAULT FALSE,
  INDEX(id)
)
```

```sql:down.sql
-- This file should undo anything in `up.sql`
DROP TABLE tasks
```

以下コマンドによって、テーブルが作成されます。
```bash
$ diesel migration run
```


#### Rustを書く

##### dbとの接続

データベース接続を確立する関数を記述します。
```rust:src/lib.rs
use diesel::mysql::MysqlConnection;
use diesel::prelude::*;
use dotenvy::dotenv;
use std::env;

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

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    MysqlConnection::establish(&database_url)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}
```
[dotenvy](https://crates.io/crates/dotenvy)クレートによって、.envファイルからデータを取得しているらしいです。
[unwrap_or_else()](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_else)の引数部分はクロージャと呼ばれるものです。[MysqlConnection::establish()](https://docs.diesel.rs/diesel/mysql/struct.MysqlConnection.html#method.establish)が失敗したときにのみクロージャ部分が実行されるようです。パフォーマンスの観点から、引数に`&str`を指定する[expect()](https://doc.rust-lang.org/std/result/enum.Result.html#method.expect)と区別しています。
参考：https://qiita.com/garkimasera/items/f39d2900f20c90d13259

##### テーブルを持つ構造体を作成する
lib.rsへ追記します。
```rust:src/lib.rs
pub mod models;
pub mod schema;
```

modelsモジュールを作成します。
```rust:src/models.rs
use diesel::prelude::*;

#[derive(Queryable, Debug)]
pub struct Task {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub done: bool,
}
```
[Queryable](https://docs.rs/diesel/2.0.2/diesel/prelude/derive.Queryable.html)というマクロはクエリから構造体を取得するためのコードを生成してくれます。
これによって、クエリの戻り値（データ）を構造体として扱えるようになります。

次に、schemaモジュールですが実は`diesel setup`を実行した際に作成されていました。
diesel.tomlにも自動的にschema情報が付与されているようです。
```rust:schema.rs
// @generated automatically by Diesel CLI.

diesel::table! {
    tasks (id) {
        id -> Integer,
        title -> Char,
        body -> Text,
        done -> Bool,
    }
}

```
これらはdiesel_cliによるマイグレーション変更時に自動更新されるようです。
[table](https://docs.rs/diesel/2.0.2/diesel/macro.table.html)マクロはテーブル名と同名のモジュールを作成します。
このモジュールには、どうやら[Table](https://docs.rs/diesel/2.0.2/diesel/query_source/trait.Table.html)トレイトや[query_dsl](https://docs.diesel.rs/diesel/query_dsl/index.html)モジュールのもつトレイトが実装されるようになるらしいです。

また先程model.rsで使用した`#[derive(Queryable)]`は、その構造体のフィールド順序がテーブルのものと一致することが前提であるため、schema.rsで定義する順序に合わせる必要があるとのことです。

##### insert

```rust:src/models.rs
use crate::schema::tasks;

#[derive(Insertable)]
#[diesel(table_name = tasks)]
pub struct NewTask<'a> {
    pub title: &'a str,
    pub body: &'a str,
}
```
[Insertable](https://docs.rs/diesel/2.0.2/diesel/prelude/derive.Insertable.html)に先程のtableマクロによって作成されたモジュールを指定することで、挿入先テーブルの情報を伝えます。

新規レコードを保存する関数を追加します。
```rust:src/lib.rs
use self::models::{NewTask, Task};

pub fn create_task(conn: &mut MysqlConnection, title: &str, body: &str) -> Task {
    use crate::schema::tasks::dsl::{id, tasks};

    let new_task = NewTask { title, body };
    diesel::insert_into(tasks)
        .values(&new_task)
        .execute(conn)
        .expect("Error saving new task");

    tasks.order(id.desc()).first(conn).unwrap()
}
```

```rust:src/bin/add_task.rs
use diesel_tutorial::*;
use std::io::{stdin, Read};

fn main() {
    let connection = &mut establish_connection();
    let mut title = String::new();
    let mut body = String::new();

    println!("What would you like your title to be?");
    stdin().read_line(&mut title).unwrap();
    // 末尾の改行を除去する
    let title = title.trim_end();

    println!(
        "\nOk! Let's write {} (Press {} when finished)\n",
        title, EOF
    );
    stdin().read_to_string(&mut body).unwrap();

    let task = create_task(connection, title, &body);
    println!("\nSaved '{}' task with id {}", title, task.id);
}

#[cfg(not(windows))]
const EOF: &str = "CTRL+D";

#[cfg(windows)]
const EOF: &str = "CTRL+Z";
```
[cfg](https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg-attribute)属性では条件付きコンパイルを指定しています。
（EOFはEndOfFileで、cfgはconfigかな）

実行してみると、レコードの追加を確認できました。
```bash
cargo run --bin add_task
```

##### select

```rust:src/bin/show_tasks.rs
use self::models::*;
use diesel::prelude::*;
use diesel_tutorial::*;

fn main() {
    use self::schema::tasks::dsl::*;

    let connection = &mut establish_connection();
    let results = tasks
        .filter(done.eq(false))
        .limit(5)
        .load::<Task>(connection)
        .expect("Error loading tasks");

    println!("Displaying {} tasks", results.len());
    for task in results {
        println!("title:\n {}", task.title);
        println!("body:\n {}", task.body);
        println!("-------------\n\n");
    }
}
```
`use self::schema::tasks::dsl::*`によって、`tasks::table`の代わりに`tasks`を、`tasks::done`の代わりに`done`を使用できるらしいです。

実行してみると、先程追加したレコードを確認できました。
```bash
cargo run --bin show_tasks
```

##### update

```rust:src/bin/switch_task.rs
use self::models::Task;
use diesel::prelude::*;
use diesel_tutorial::*;
use std::env::args;

fn main() {
    use self::schema::tasks::dsl::{done, tasks};

    let id = args()
        .nth(1)
        .expect("switch_task requires a task id")
        .parse::<i32>()
        .expect("Invalid iD");
    let connection = &mut establish_connection();

    let task: Task = tasks
        .find(id)
        .limit(1)
        .get_result(connection)
        .unwrap_or_else(|_| panic!("Unable to find task {}", id));
    let is_finished = task.done;

    diesel::update(tasks.find(id))
        .set(done.eq(!is_finished))
        .execute(connection)
        .unwrap();
    println!("Switched {} to {}", task.title, !is_finished);
}

```

実行してみると、レコードの更新を確認できました。
```bash
cargo run --bin switch_task 1.
cargo run --bin show_tasks
```

##### delete

```rust:src/bin/clean_task.rs
use diesel::prelude::*;
use diesel_tutorial::*;
use std::env::args;

fn main() {
    use self::schema::tasks::dsl::{tasks, title};

    let target = args().nth(1).expect("Expected a target to match against");
    let pattern = format!("%{}%", target);

    let connection = &mut establish_connection();
    let num_deleted = diesel::delete(tasks.filter(title.like(pattern)))
        .execute(connection)
        .expect("Error deleting tasks");

    println!("Deleted {} tasks", num_deleted);
}
```
実行してみると、レコードの削除を確認できました。

```bash
cargo run --bin clean_task demo
cargo run --bin show_tasks
```


## おわりに

dieselのチュートリアルをmysqlでやってみました(ほぼ写経・・)。
細かい所でいうとチュートリアルはpostgresqlでされているので、Returnig句の有無による実装の差([関連issue](https://github.com/diesel-rs/diesel/issues/2535))や(当然ですが)文脈が違うことによる単語の差異について考えさせられました。
Rustについては(特にクロージャ、マクロへの)理解不足感がにじみ出ております。
VSCode拡張の[rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)が良い感じだったので、初学者には特におすすめです。