Rust + MySQL + Diesel(1)
RustでDBを操作する方法を勉強中なので、勉強ついでにまとめていきます。
まずは、基本的なCRUD処理について学んだ内容をまとめていきます。
学習用のため、できる限り丁寧にまとめていきますが、分からない点、また間違え等あればご指摘いただければ幸いです。
前提
動作確認環境は以下の通りです。
Ubuntu 18.04
Rust 1.46.0
MySQL 8.0.21
Diesel CLI のインストール
RustでDB管理をサポートするDiesel CLIをインストールします。
今回はMySQLとの依存関係のみインストールするので、
–no-default-features –features mysql
のオプションを追加しています。
$ cargo install diesel_cli --no-default-features --features mysql
...
$ diesel -V
diesel 1.4.1
※自分の環境では、下記のようなエラーが表示され、インストールに失敗しました。
...
= note: /usr/bin/ld: cannot find -lmysqlclient
collect2: error: ld returned 1 exit status
...
Caused by:
could not compile `diesel_cli`.
To learn more, run the command again with --verbose.
これは、「/etc/ld.so.conf.d/*.conf」に記載されているパスに「libmysqlclient.so*」が無いことが原因のようです。
下記パッケージインストールを実行することで解決することが確認できました。
$ sudo apt install libmysqlclient-dev
...
$ sudo find / -name "libmysqlclient.so*"
/usr/lib/x86_64-linux-gnu/libmysqlclient.so
/usr/lib/x86_64-linux-gnu/libmysqlclient.so.21
/usr/lib/x86_64-linux-gnu/libmysqlclient.so.21.1.21
プロジェクトの作成
cargoコマンドでRustプロジェクトを作成します。
$ cargo new rust_mysql
Created binary (application) `rust_mysql` package
$ cd rust_mysql
rust_mysql/.envを作成し、DBへのアクセスURLを記入します。
なお、この際使用するDBは新規作成しています。
記入例:DATABASE_URL=mysql://user_name:password@localhost/testdb
DATABASE_URL=mysql://[ユーザー名]:[パスワード]@[IPアドレス]/[データベース名]
Diesel CLIを使ってDieselのセットアップを実行します。
$ diesel setup
Creating migrations directory at: /home/user/workspace/rust_mysql/migrations
$ diesel migration generate rust_mysql_init
Creating migrations/2020-10-15-233022_rust_mysql_init/up.sql
Creating migrations/2020-10-15-233022_rust_mysql_init/down.sql
現時点でのディレクトリ構成は以下の通りです。(git関連は記入していません)
rust_mysql
├─ migrations
│ └─ 2020-10-15-233022_rust_mysql_init
│ ├─ up.sql
│ └─ down.sql
├─ src
│ └─ main.rs
├─ .env
├─ Cargo.toml
└─ diesel.toml
マイグレーションの実行
up.sql、down.sqlを書いて、マイグレーションを行います。
まずはup.sqlに、マイグレーションに適用するSQL文を書いていきます。
今回はユーザーテーブルを作成しています。
CREATE TABLE users
(
id SERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL
);
次にdown.sqlに、マイグレーションの適用前に戻すためのSQL文を書いていきます。
DROP TABLE users;
最後に、コマンドでマイグレーションを実行します。
$ diesel migration run
Running migration 2020-10-15-233022_rust_mysql_init
マイグレーションを実行すると.envで設定したDBにユーザーテーブルが作成され、rust_mysql/src/schema.rsが作成されます。このファイルはRust内でテーブルアクセスする際に利用します。
今回、schema.rsの中身はこうなりました。
table! {
users (id) {
id -> Unsigned<Bigint>,
name -> Varchar,
}
}
diesel migration run コマンドは、up.sqlのSQL文を実行します。
INSERT文などを記入すれば、レコードを作成することも可能でした。
diesel migration redo コマンドは、down.sql → up.sql の順にSQL文を実行します。
今回の場合だと、ユーザーテーブルの削除 → 再度作成することになります。
diesel migration revert コマンドは、down.sqlのSQL文を実行します。
※以下コマンドの流れで、もう少し正確に各コマンドについて考えてみます。
(今回使うDBでは以下コマンドは実行していません。)
$ diesel migration generate test1
Creating migrations/2020-10-18-205348_test1/up.sql
Creating migrations/2020-10-18-205348_test1/down.sql
$ diesel migration generate test2
Creating migrations/2020-10-18-205353_test2/up.sql
Creating migrations/2020-10-18-205353_test2/down.sql
$ diesel migration generate test3
Creating migrations/2020-10-18-205355_test3/up.sql
Creating migrations/2020-10-18-205355_test3/down.sql
$ diesel migration run
Running migration 2020-10-18-205348_test1
Running migration 2020-10-18-205353_test2
Running migration 2020-10-18-205355_test3
$ diesel migration revert
Rolling back migration 2020-10-18-205355_test3
$ diesel migration redo
Rolling back migration 2020-10-18-205353_test2
Running migration 2020-10-18-205353_test2
$ diesel migration revert
Rolling back migration 2020-10-18-205353_test2
$ diesel migration run
Running migration 2020-10-18-205353_test2
Running migration 2020-10-18-205355_test3
ここから
diesel migration generate:マイグレーションポイントの作成
diesel migration run:最新のマイグレーションポイントまでのup.sqlの実行
diesel migration redo:現在のマイグレーションポイントのdown.sql、up.sqlの実行
diesel migration revert:現在のマイグレーションポイントのdown.sqlを実行し、一つ前のポイントに戻す
という認識で良いかと思います。
このマイグレーションポイントについては、接続しているDBに以下のような形で保存されます。この際保存されているマイグレーションポイントは、最新の、ではなく現在のポイントより以前のものとなります。
mysql> select * from __diesel_schema_migrations;
+----------------+---------------------+
| version | run_on |
+----------------+---------------------+
| 20201018205348 | 2020-10-19 05:54:12 |
| 20201018205353 | 2020-10-19 05:54:37 |
| 20201018205355 | 2020-10-19 05:54:37 |
+----------------+---------------------+
diesel migration run コマンドはフォルダ名(2020-10-18-205348_test1など)から順次より新しいマイグレーションポイントを発見して、up.sqlの実行とマイグレーションポイントのレコードを登録していき、
diesel migration revert コマンドはDBから実際にup.sqlが実行されたポイントを見つけ、それに対応するdown.sqlを実行してレコードを削除する、という流れのようです。
つまり、__diesel_schema_migrationsテーブルはup.sqlの実行されたポイントを記録しているものであると考えられます。
Dieselの導入
Cargo.tomlを編集し、dieselクレートを依存関係に追加します。
...
[dependencies]
diesel = { version = "1.4.4", features = ["mysql"] }
dotenv = "0.15.0"
以下コマンドを実行し、クレートのダウンロード、コンパイルを行います。
$ cargo build
...
Finished dev [unoptimized + debuginfo] target(s) in 1m 47s
ここで今後のために少しsrcフォルダ内の構成を変え、ファイルを作成します。
構成は以下の通りです。
rust_mysql
├─ src
│ ├─ bin ← 新規作成(フォルダ)
│ │ └─ main.rs ← ファイル移動
│ ├─ lib.rs ← 新規作成
│ ├─ models.rs ← 新規作成(空)
│ ├─ schema.rs
│ └─ utils.rs ← 新規作成
│
[以下略]
各フォルダ、ファイルの役割は以下の通りです。
bin:バイナリクレート(実行ファイルとするファイル)をまとめる
lib.rs:モジュールツリーを作成し、それぞれの公開範囲を管理する
models.rs:DBの操作に用いる構造体をまとめる
utils.rs:共通操作(今回ではDBとの接続インスタンスの生成)をまとめる
lib.rsの中身
#[macro_use]
extern crate diesel;
pub mod models;
pub mod schema;
pub mod utils;
utils.rsの中身
use diesel::mysql::MysqlConnection;
use diesel::prelude::*;
use dotenv::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)
.expect(&format!("Error connecting to {}", database_url))
}
一つずつ処理を追っていきます。
dotenv().ok();
.envファイルの中身の変数を取得し、環境変数として使用できるようにします。
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
環境変数DATABASE_URLの値を取得し、database_urlに代入します。
(DATABASE_URLを設定していないとエラー)
MysqlConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
DBとの接続インスタンスを確立し、呼び出し元に返します。
(DBとの接続に失敗するとエラー)
ここまでで準備が完了しました。
Create
マイグレーションによって作成されたテーブルに、新規レコードを追加する方法について学んでいきます。
まず、DBテーブルへデータを追加するため、データをまとめる入れ物として構造体を定義します。
構造体の定義はmodels.rsでしていきます。
use crate::schema::users;
#[derive(Insertable)]
#[table_name = "users"]
pub struct NewUser {
pub name: String,
}
まず、useコマンドでユーザーテーブルのスキーマを取得します。
データ追加用の構造体はInsertableトレイトを継承する必要があり、その際にtable_nameとして対象のスキーマを設定する必要があります。
構造体は、INSERTする際に必要なフィールドを持っており、その型は各対応カラムの型に合わせて設定します。
カラムの型についてはschema.rsを見ると確認できますが、その型はdiesel内で定義されている型であり、構造体にそのまま使うわけではありません。
今回の場合は
Varchar → String
として構造体のフィールドの型としています。
対応する型について分からない場合は、「rust diesel 型名」などで検索すれば分かるかもしれません。
※Varcharに対応する型はTextであれば良く、String型だけでなく&str型でも大丈夫です。
ただしその場合、明示的なライフタイム注釈が必要となります。(以下コード参考)
また、String型の時に行なわれる所有権の移行がなくなるため、NewUser構造体のインスタンスがスコープから外れるだけではメモリの解放が行われず、注意が必要です。
#[derive(Insertable)]
#[table_name = "users"]
pub struct NewUser<'a> {
pub name: &'a str,
}
では実際に、構造体を使ってレコードの追加を実行していきます。
1件追加
rust_mysql/src/bin/insert_user.rsを作成します。
use diesel::prelude::*;
use rust_mysql::models::NewUser;
use rust_mysql::schema::users as users_schema;
use rust_mysql::utils::establish_connection;
fn main() {
let connection = establish_connection();
let new_user = NewUser {
name: String::from("new_user"),
};
diesel::insert_into(users_schema::dsl::users)
.values(new_user)
.execute(&connection)
.expect("Error saving new user");
}
一つずつ処理を追っていきます。
let connection = establish_connection();
utils.rsのestablish_connection関数から、DBとの接続インスタンスを取得します。
let new_user = NewUser {
name: String::from("new_user"),
};
models.rsで定義したNewUser構造体のインスタンスを生成します。
この時、String型の値の所有権ごと構造体に渡します。
diesel::insert_into(users_schema::dsl::users)
.values(&new_user)
.execute(&connection)
.expect("Error saving new user");
インサート処理を実行します。
SQLでのINSERT文をイメージしてもらえると分かりやすいかと思います。
valuesにnew_userの参照を渡していますが、以降使わないのであれば参照である必然性はありません。
※MySQLではなくPostgreSQL、SQLiteを用いる場合、executeの代わりにget_resultを用いることができます。
その場合、返り値として実際にインサートした値を取得することができるようです。
これはPostgreSQLなどで使えるRETURNING句を用いているものだと考えられます。
では実際に処理を実行していきます。
バイナリクレートが複数ある場合、コマンドの中で実行するバイナリを指定します。
$ cargo run --bin insert_user
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/insert_user`
実行後にMySQLに接続し確認してみると、ユーザーテーブルにnew_userが追加されていることが分かります。
mysql> select * from users;
+----+----------+
| id | name |
+----+----------+
| 1 | new_user |
+----+----------+
※構造体を使わなくても、インサート処理を実行することはできます。
ただし、コードの可読性などの観点から、構造体を利用した方が明確で良いと個人的に考えています。
構造体を使わない場合、例えば以下のコードでインサート処理が可能です。
diesel::insert_into(users_schema::dsl::users)
.values((&users_schema::name.eq("example_user")))
.execute(&connection)
.expect("Error saving new user");
複数件追加
インサート処理において、valuesに構造体のベクタを渡すと、複数件をまとめてインサートすることができます。
insert_user.rsを少し編集します。
...
fn main() {
let connection = establish_connection();
// 変更
let new_users = vec![
NewUser {
name: String::from("new_users1"),
},
NewUser {
name: String::from("new_users2"),
},
];
diesel::insert_into(users_schema::dsl::users)
// 変更
.values(&new_users)
.execute(&connection)
.expect("Error saving new users");
}
先程のNewUser型の変数new_userを、Vec<NewUser>の変数new_usersに置き換えました。
これを同様に実行すると、同時に2件のレコードが追加されます。
$ cargo run --bin insert_user
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/insert_user`
-----------------------------------
MySQL接続
-----------------------------------
mysql> select * from users;
+----+------------+
| id | name |
+----+------------+
| 1 | new_user |
| 2 | new_users1 |
| 3 | new_users2 |
+----+------------+
Read
DBテーブルからデータを読み込む方法について学んでいきます。
まず、DBテーブルから読み込むデータを格納するための入れ物として構造体を定義します。
構造体の定義はmodels.rsに追記していきます。
...
#[derive(Debug, Queryable)]
pub struct User {
pub id: u64,
pub name: String,
}
Debugトレイトは、構造体のフィールドの確認用に継承しています。
また、Queryableトレイトは、データの読み込みに利用するために継承する必要があります。
構造体のフィールドの型には、id(Unsigned<Bigint>型)に対応するu64型に設定しています。
では実際に、構造体を使ってレコードの読み込みを実行していきます。
1件読み込み
rust_mysql/src/bin/show_user.rsを作成します。
use diesel::prelude::*;
use rust_mysql::models::User;
use rust_mysql::schema::users as users_schema;
use rust_mysql::utils::establish_connection;
fn main() {
let connection = establish_connection();
let user = users_schema::dsl::users
.first::<User>(&connection)
.expect("Error loading users");
println!("{:?}", user)
}
新たな処理を確認します。
let user = users_schema::dsl::users
.first::<User>(&connection)
.expect("Error loading users");
users_schema::dsl::users でユーザーテーブルを指定し、その先頭レコードをUser型で返しています。
では実際に実行してみます。
$ cargo run --bin show_user
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/show_user`
User { id: 1, name: "new_user" }
先頭レコード(デフォルトでは最も先にテーブルに追加されたレコード)を読み込むことができました。
複数件読み込み
firstメソッドをloadメソッドに置き換えることで、複数件のデータをベクタ型で受け取ることができます。
show_user.rsを少し編集します。
...
fn main() {
let connection = establish_connection();
//変更
let users = users_schema::dsl::users
.load::<User>(&connection)
.expect("Error loading users");
//変更
for user in users {
println!("{:?}", user);
}
}
この時、usersはUser型ではなく、Vec<User>型を受け取ります。
実際に実行してみます。
$ cargo run --bin show_user
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/show_user`
User { id: 1, name: "new_user" }
User { id: 2, name: "new_users1" }
User { id: 3, name: "new_users2" }
ユーザーテーブルのデータの一覧を取得することができました。
※検索条件について
メソッドを追加することで、SQL文のように検索条件等を追加していくことができます。
いくつかここで例をあげますが、詳細は
https://docs.diesel.rs/diesel/query_dsl/trait.QueryDsl.html
を確認すると良いかと思います。
show_user.rsを編集し、確認していきます。
...
let users = users_schema::dsl::users
.limit(2)
.order(users_schema::name.desc())
.filter(users_schema::name.eq("new_user"))
.or_filter(users_schema::name.like("%2"))
.find(3)
.load::<User>(&connection)
.expect("Error loading users");
...
一例ではありますが、上のコードで使っている条件等と、対応するSQLを確認していきます。。
limit():LIMIT句
order():ORDER BY句
users_schema::name.desc():users.name DESC
filter():WHERE句
users_schema::name.eq(“new_user”):users.name = “new_user”
or_filter():WHERE句(OR条件)
users_schema::name.like(“%2”):users.name LIKE “%2”
find(3):WHERE [主キー] = 3
直感的にSQLのように条件指定ができるようです。
UPDATE
DBテーブルのデータを更新する方法について学んでいきます。
rust_mysql/src/bin/update_user.rsを作成します。
use diesel::prelude::*;
use rust_mysql::schema::users as users_schema;
use rust_mysql::utils::establish_connection;
fn main() {
let connection = establish_connection();
diesel::update(users_schema::dsl::users.find(2))
.set(users_schema::name.eq("update_user"))
.execute(&connection)
.expect("Error updating users");
}
新たな処理を確認します。
diesel::update(users_schema::dsl::users.find(2))
.set(users_schema::name.eq("update_user"))
.execute(&connection)
.expect("Error updating users");
ユーザーテーブルの主キー(id)が2のレコードについて、nameカラムの値をupdate_userに更新しています。
users_schema::dsl::usersがテーブル全体を指しており、その後に条件メソッドを追加していると考えることができます。
.find(2)を、例えば無くせばテーブル全体のレコードに対して更新処理を行いますし、.filter(users_schema::name.eq(“new_user”))と変えれば、users.nameの値がnew_userのレコードについて更新処理を行います。
setメソッドでは、ユーザーテーブルの名前カラムの値をupdate_userに更新することを指定しています。
では実際に実行してみます。
$ cargo run --bin update_user
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/update_user`
-----------------------------------
MySQL接続
-----------------------------------
mysql> select * from users;
+----+------------+
| id | name |
+----+------------+
| 1 | new_user |
| 2 | update_user |
| 3 | new_users2 |
+----+------------+
主キー(id)が2のレコードのnameを、update_userに更新することができました。
DELETE
DBテーブルのデータを削除する方法について学んでいきます。
rust_mysql/src/bin/delete_user.rsを作成します。
use diesel::prelude::*;
use rust_mysql::schema::users as users_schema;
use rust_mysql::utils::establish_connection;
fn main() {
let connection = establish_connection();
diesel::delete(users_schema::dsl::users.find(1))
.execute(&connection)
.expect("Error deleting users");
}
新たな処理を確認します。
diesel::delete(users_schema::dsl::users.find(1))
.execute(&connection)
.expect("Error deleting users");
DELETE処理を行うレコードの指定については、UPDATE処理の時と全く同じです。
では実際に実行してみます。
$ cargo run --bin delete_user
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/delete_user`
-----------------------------------
MySQL接続
-----------------------------------
mysql> select * from users;
+----+------------+
| id | name |
+----+------------+
| 2 | update_user |
| 3 | new_users2 |
+----+------------+
主キー(id)が1のレコードを削除することができました。
終わりに
今回は最も基本的なところであるCRUD処理について確認しました。
次回はテーブルの結合、トランザクション処理などを確認していきます。
Tips
スキーマついて
schema::users::dsl::users と schema::users::table
は同じような扱い方ができるようです。(コードを置き換えることができます。)
同様に、
schema::users::dsl::name と schema::users::name
も同じよう扱い方ができます。
この記事へのコメントはありません。