RustでDieselを使いSQLiteにDB接続してみる

# 1. はじめに
## 1-1. DieselのGetting Startedを試す
Rustでよく使われるDB接続ツールは、[Diesel](https://diesel.rs/)のようです。

Dieselのコンセプトは、
> *A safe, extensible ORM and Query Builder for Rust*

とのこと。いまのところサポートしているデータベースは、

- PostgreSQL
- MySQL
- SQLite

の３種類です。

今回はこのDieselのGetting Startedを試してみたいと思います。

##:computer:環境情報:computer:

この記事は以下の環境で試しています。

- Diesel 1.4.5
- Windows ver.1903
- Visual Studio Code 1.49.1
- Rust 1.46.0

環境の準備はこちらの記事に書きました。
[Visual Studio CodeでRust開発環境を整える](https://qiita.com/84zume/items/377033ab6b6aee2a68d7)

# 2. Getting Startedを実施する
Diesel公式サイトの[Getting Started](https://diesel.rs/guides/getting-started/)を動かします。

##:warning:注意点:warning:
- 公式サイトは、PostgreSQLを題材にしていますが、**SQLite**で試したかったため、以下はSQLiteでのGetting Startedになります。 
- けっこう詰まったので、コードも全量載せます。
- バージョンも執筆時点での最新を利用します。

##2-1.SQLiteをインストールする
Chocolateyを利用してCUIでSQLiteをインストールします。Chocolatey自体のインストールはこちらを参照して下さい→[Installing Chocolatey](https://chocolatey.org/install)

```powershell:コマンド
# 管理者権限で実行
choco install sqlite
```

##2-2.プロジェクトとDBを作成する
`cargo new` で今回のプロジェクトを作成します。

```powershell:コマンド
cargo new --lib diesel_sample
cd .\diesel_sample\
```

`Cargo.toml'に関連するモジュールの依存関係を追加する。

```toml:Cargo.toml
[dependencies]
diesel = { version = "1.4.5", features = ["sqlite", "chrono"] }
libsqlite3-sys = { version = "0.9.1", features = ["bundled"]}
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
```

DieselのCLIツールをインストールする。

```powershell:コマンド
cargo install diesel_cli --no-default-features --features "sqlite-bundled"
# ～略～
#   Installed package `diesel_cli v1.4.1` (executable `diesel.exe`)  
```

DBの接続情報を作成し、`diesel setup`でDBと空のマイグレーションディレクトリを作成する。
Getting Startedでは`.env`ファイルに環境情報を記載するとあるが、なぜかdieselコマンドが`.env`を読み込んでくれないため引数で渡しました。

```powershell:コマンド
diesel setup --database-url=sample.db

# Creating migrations directory at: D:\Develop\code-writing\rust\diesel_sample\migrations
# Creating database: sample.db
```

マイグレーション用の空SQLファイルを作成する。up.sqlが作成用で、down.sqlが削除用。

```powershell:コマンド
diesel migration generate create_posts

# Creating migrations\2020-09-25-150111_create_posts\up.sql
# Creating migrations\2020-09-25-150111_create_posts\down.sql
```

up.sqlとdown.sqlをそれぞれ下記の内容で作成する。

```sql:up.sql
CREATE TABLE posts (
  id INTEGER NOT NULL PRIMARY KEY,
  title VARCHAR NOT NULL,
  body TEXT NOT NULL,
  published BOOLEAN NOT NULL DEFAULT 0
)
```

```sql:down.sql
DROP TABLE posts
```

では、マイグレーションを実行し、テーブルを作成します。
この際に後で出てくる`schema.rs`も自動生成されるようです。

```powershell:コマンド
diesel migration run --database-url=sample.db 

# Running migration 2020-09-25-234318_create_posts
```

##2-3.アプリを作成する
作成したDBに対する単純なCRUD操作をするアプリをコーディングします。

`lib.rs`でコネクション管理する。

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

pub mod models;
pub mod schema;

use self::models::NewPost;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use dotenv::dotenv;

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

    let database_url = "sample.db";
    SqliteConnection::establish(&database_url)
        .expect(&format!("Error connecting to {}", database_url))
}

pub fn create_post(conn: &SqliteConnection, title: &str, body: &str) -> usize {
    use crate::schema::posts;

    let new_post = NewPost { title, body };

    diesel::insert_into(posts::table)
        .values(&new_post)
        //SQLiteはget_result()は対応していないため、execute()
        .execute(conn)
        .expect("Error saving new post")
}

```

モデルとスキーマを作成する。ただし、スキーマは前述通り自動生成されているはず。

```rust:models.rs
use super::schema::posts;

#[derive(Queryable)]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub published: bool,
}

#[derive(Insertable)]
#[table_name = "posts"]
pub struct NewPost<'a> {
    pub title: &'a str,
    pub body: &'a str,
}
```

```rust:schema.rs
table! {
    posts (id) {
        id -> Integer,
        title -> Text,
        body -> Text,
        published -> Bool,
    }
}
```

CRUD操作部分を実装。`bin`フォルダに作ります。
`write_post.rs`にて標準入力からデータを読み取り、draftとしてデータを作成する。

```rust:bin\write_post.rs
use diesel_sample::*;
use std::io::{stdin, Read};

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

    println!("What would you like your title to be?");
    let mut title = String::new();
    stdin().read_line(&mut title).unwrap();
    let title = &title[..(title.len() - 1)]; // Drop the newline character
    println!(
        "\nOk! Let's write {} (Press {} when finished)\n",
        title, EOF
    );
    let mut body = String::new();
    stdin().read_to_string(&mut body).unwrap();

    let _ = create_post(&connection, title, &body);
    println!("\nSaved draft {}", title);
}

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

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

```

`publish_post.rs`でDBにデータを登録

```rust:publish_post.rs
use diesel::prelude::*;
use diesel_sample::*;
use std::env::args;

fn main() {
    use diesel_sample::schema::posts::dsl::{posts, published};

    let id = args()
        .nth(1)
        .expect("publish_post requires a post id")
        .parse::<i32>()
        .expect("Invalid ID");
    let connection = establish_connection();

    let _ = diesel::update(posts.find(id))
        .set(published.eq(true))
        .execute(&connection)
        .unwrap();

    let post: models::Post = posts
        .find(id)
        .first(&connection)
        .unwrap_or_else(|_| panic!("Unable to find post {}", id));

    println!("Published post {}", post.title);
}

```

`show_posts.rs` でDBに格納されているデータを取得して、標準出力へ表示。

```rust:show_posts.rs
extern crate diesel;
extern crate diesel_sample;

use self::diesel::prelude::*;
use self::diesel_sample::*;
use self::models::*;

fn main() {
    use diesel_sample::schema::posts::dsl::*;

    let connection = establish_connection();
    let results = posts
        .filter(published.eq(true))
        .limit(5)
        .load::<Post>(&connection)
        .expect("Error loading posts");

    println!("Displaying {} posts", results.len());
    for post in results {
        println!("{}", post.title);
        println!("----------\n");
        println!("{}", post.body);
    }
}

```

`delete_post.rs`でタイトルにパターンマッチさせて合致したものを削除する。

```rust:delete_post.rs
use diesel::prelude::*;
use diesel_sample::*;
use std::env::args;

fn main() {
    use diesel_sample::schema::posts::dsl::*;

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

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

    println!("Deleted {} posts", num_deleted);
}

```

ここまで実装すると最終的にはこんなファイル構成になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34410/f7d83934-0da6-9b6c-8220-5bf695464e83.png)



##2-4.完成したアプリを動かす
CRUD操作を試してみます。

```powershell:コマンド
cargo run --bin show_posts

#   Compiling diesel_sample v0.1.0 (D:\***\diesel_sample)
#     Running `target\debug\show_posts.exe`
 
# Displaying 0 posts
# ↑データはまだないので0件

cargo run --bin write_post
#   Compiling diesel_sample v0.1.0 (D:\***\diesel_sample)
#    Running `target\debug\write_post.exe`

# What would you like your title to be?
# Diesel sample
# ↑Diesel sampleを入力

# (Press CTRL+Z when finished)
# ↑CTRL+Zを入力

cargo run --bin publish_post 1

#   Compiling diesel_sample v0.1.0 (D:\***\diesel_sample)
#    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
#     Running `target\debug\publish_post.exe 1`
#     Running `target\debug\show_posts.exe`
# Displaying 1 posts
# Diesel sample
# ----------
# ↑入力した情報が出力された！

cargo run --bin delete_post sample

#  Compiling diesel_sample v0.1.0 (D:\Develop\code-writing\rust\diesel_sample)   
#    Finished dev [unoptimized + debuginfo] target(s) in 0.49s
#     Running `target\debug\delete_post.exe sample`
# Deleted 1 posts
# ↑入力した情報が削除された！
```


#3.おわりに

Windows環境であること、SQLiteで試したこと、で結構はまってしまいました。
まだ依存関係のところがもやっとしていますが、Getting Startedを試してみるということにおいては目的を果たせたかなと思います。

#参考
- [Install Rust Diesel in Windows 10 and Fix Issue](https://www.yodiw.com/install-rust-diesel-in-windows-10-and-fix-issue)