この記事は「【マイスター・ギルド】本物のAdvent Calendar 2022」7日目の記事です。
はじめに
Rustのアウトプットとして、dieselのチュートリアルを使ってCRUD機能を試してみました。
全てチュートリアルレベルなので、初学者向きです。
本編
環境構築
docker-composeを使用しました。
docker単体で構築する場合は、コンテナ間network接続時にservice名を使用できないため名前付きネットワークを定義してIPを固定したりする必要があるかもしれないです。
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つのライブラリもしくはいくつかの実行ファイル諸々がまとまった単位です)
[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を編集します。
-- 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)
)
-- This file should undo anything in `up.sql`
DROP TABLE tasks
以下コマンドによって、テーブルが作成されます。
$ diesel migration run
Rustを書く
dbとの接続
データベース接続を確立する関数を記述します。
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へ追記します。
pub mod models;
pub mod schema;
modelsモジュールを作成します。
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情報が付与されているようです。
// @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
use crate::schema::tasks;
#[derive(Insertable)]
#[diesel(table_name = tasks)]
pub struct NewTask<'a> {
pub title: &'a str,
pub body: &'a str,
}
Insertableに先程のtableマクロによって作成されたモジュールを指定することで、挿入先テーブルの情報を伝えます。
新規レコードを保存する関数を追加します。
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()
}
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
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
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
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が良い感じだったので、初学者には特におすすめです。