LoginSignup
17
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-09

この記事は「【マイスター・ギルド】本物のAdvent Calendar 2022」7日目の記事です。

はじめに

Rustのアウトプットとして、dieselのチュートリアルを使って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
FROM rust:1.65-buster

WORKDIR /home/yuhei3017/diesel_tutorial
COPY . .

ENTRYPOINT ["bash"]

まずは、新しいプロジェクトを開始します。
CargoとはRustのパッケージマネジャ兼ビルドツールです。

$ cargo new --lib diesel_tutorial

最終的なファイル構成も載せておきます。

.
├── 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のインストールです。
diesel_cliは必須ではありませんが

  • マイグレーションファイルの実行
  • データベーススキーマを表すrustファイルの自動作成
    をサポートしています。
cargo install diesel_cli

実装

データベース接続

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

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

diesel setupによって、
appコンテナからdbコンテナのmysqlへ接続し、データベースが作成されます。
また、カレントへmigrationディレクトリが作成されます。

root@49fd6e0fd870:/home/yuhei3017/diesel_tutorial# diesel setup
Creating database: diesel_tutorial

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

以下のコマンドによって、マイグレーションファイルが作成されます。

$ 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を編集します。

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)
)
down.sql
-- This file should undo anything in `up.sql`
DROP TABLE tasks

以下コマンドによって、テーブルが作成されます。

$ diesel migration run

Rustを書く

dbとの接続

データベース接続を確立する関数を記述します。

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クレートによって、.envファイルからデータを取得しているらしいです。
unwrap_or_else()の引数部分はクロージャと呼ばれるものです。MysqlConnection::establish()が失敗したときにのみクロージャ部分が実行されるようです。パフォーマンスの観点から、引数に&strを指定するexpect()と区別しています。
参考:https://qiita.com/garkimasera/items/f39d2900f20c90d13259

テーブルを持つ構造体を作成する

lib.rsへ追記します。

src/lib.rs
pub mod models;
pub mod schema;

modelsモジュールを作成します。

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というマクロはクエリから構造体を取得するためのコードを生成してくれます。
これによって、クエリの戻り値(データ)を構造体として扱えるようになります。

次に、schemaモジュールですが実はdiesel setupを実行した際に作成されていました。
diesel.tomlにも自動的にschema情報が付与されているようです。

schema.rs
// @generated automatically by Diesel CLI.

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

これらはdiesel_cliによるマイグレーション変更時に自動更新されるようです。
tableマクロはテーブル名と同名のモジュールを作成します。
このモジュールには、どうやらTableトレイトやquery_dslモジュールのもつトレイトが実装されるようになるらしいです。

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

insert
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に先程のtableマクロによって作成されたモジュールを指定することで、挿入先テーブルの情報を伝えます。

新規レコードを保存する関数を追加します。

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()
}
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属性では条件付きコンパイルを指定しています。
(EOFはEndOfFileで、cfgはconfigかな)

実行してみると、レコードの追加を確認できました。

cargo run --bin add_task
select
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を使用できるらしいです。

実行してみると、先程追加したレコードを確認できました。

cargo run --bin show_tasks
update
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);
}

実行してみると、レコードの更新を確認できました。

cargo run --bin switch_task 1.
cargo run --bin show_tasks
delete
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);
}

実行してみると、レコードの削除を確認できました。

cargo run --bin clean_task demo
cargo run --bin show_tasks

おわりに

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

17
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
7