1. HOME
  2. ブログ
  3. Rust
  4. Rust + MySQL + Diesel(1)

技術ブログ

Blog

Rust

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
も同じよう扱い方ができます。

  1. この記事へのコメントはありません。

  1. この記事へのトラックバックはありません。

関連記事