Rustで使えるORMのDieselをバージョン1系からバージョン2系にアップグレードする際の注意点

DieselはRustで使えるORMです。

Railsで言う所のActive Recordのようなものだと思って頂ければ良いです。

RustでWebアプリケーションを構築する際に必然的に候補に挙がると思います。

バージョン2系からより便利な機能が追加されました。

バージョン2.1系ではMultiConnectionもサポートしています。

コネクションタイプをリクエスト時に切り替えることができるようになっているため複数種類のデータベースに跨ったシステムを構築できます。

機能追加に伴いバージョン1系と比べると実装に変化があります。

特に1番大きい変化は、データベースへのリクエスト時に渡すコネクションの値がミュータブル(可変)でなくてはならなくなりました。

アップグレードを行った際に私自身かなり解決に時間を必要とした部分です。

Rustはメモリ管理の概念が仕様レベルで他の言語と根本的に異なります。

そのため他の言語で当たり前のようにできたことがRustではできないことも多いです。ただその難しさと引き換えにメモリ安全かつ高速なアプリケーションを構築することができるのが Rust の魅力です。

例えば、RubyPHPJavaのような言語では宣言した変数の値を変更することは普通にできます。変更できないようにするにはconstのような宣言が必要だったりします。

Rubyならfreezeでしょうか)

Rustはその考え方の逆でmutを付けずに宣言した変数はすべてイミュータブル(不変)です。つまり初期化時に設定した値は変更不可な状態がデフォルトです。

大抵の…あるいは大半のプログラムは変数を使い回せるという仕様がバグを生む要因の傾向として多いと考えます。

Rustはデフォルトで不変の変数しか扱えなくすることで根本レベルでバグを防ぐ仕様になっているのです。

本題に入ります。

Dieselの1系まではデータベースへのリクエスト時に渡すコネクションはイミュータブルでした。そのため以下のようなコードが通りました。

pub struct Repository {
    connection: diesel::mysql::MysqlConnection
}

impl Repository {
    pub fn new() -> Self {
        Self {
            connection: MysqlConnection::establish("mysql://user:password@localhost:3306/database") 
        }
    }

    pub fn account_exists(&self, _email: &str) -> Result<bool> {
        match accounts.filter(email.eq(_email)).load::<Account>(&self.connection) {
            ~ 省略 ~
        }
    }
}

構造体にコネクションを保持するメンバを定義して構造体初期化時にコネクションを生成、トレイトにてメンバに保存されたコネクションを使用してデータベースへリクエストを行うコード例です。

データベースへのリクエスト時はイミュータブルなコネクションの参照を渡せばよいので & を付けて渡せばよいのです。

ですがバージョン2系からはデータベースへのリクエスト時に渡すコネクションの値はミュータブルである必要があります。

Rustを学ぶと分かりますが、 Rustは構造体のフィールドにミュータブルのメンバ変数を持つことはできません。

すなわち上記に示したような、コネクションを外部から注入するのではなく構造体自身に持たせる実装だと恐らく1系から2系にアップグレードした際にコンパイルエラーになります。

コンパイルを通るようにするには…

  • 構造体にデータベースへのコネクションを持たせずに引数等を使用して外部からコネクションを引き渡してクエリをリクエストする
  • 何とか構造体にデータベースへのコネクションの値を持たせたうえでクエリをリクエストする

のどちらかで解決する必要があります。

大抵の人は…やっぱり構造体に持たせたい訳です。オブジェクト指向的に組みたくなる訳です。

仮に外部から注入するにせよメソッドの引数にしない限りは構造体に格納することになりますがそこでもRustの制約に引っ掛かるだけです。

私は以下に示す方法で解決しました。

構造体にミュータブルの値をメンバとして持たせたいという需要はあるので以下の機能がRustには標準で用意されています。

RefCell

use std::cell::RefCellを宣言することで使用できます。

RefCellは実行時に可変性をチェックすることで単一のスレッド内での可変な借用を可能にする機能が含まれるクレートです。 Rustは所有権と借用のルールがコンパイル時にチェックされるため制約が厳しいです。RefCell はそういった制約を緩和するための手段を提供しています。

RefCellは内部の値への不変な参照(&T)を取得しそれを可変な参照(&mut T)に昇格させるとができます。 コンパイル時ではなく実行時に昇格させるのでコンパイルを通すことができるのです。

勘の良い方は気付くかもですが…これを使うことによってコンパイルは通りますがメモリ安全であるかどうかはまったく別の課題です。 コンパイルが防いでくれていたメモリ安全の部分を回避させた訳ですから実行時に安全かどうかは実装に依存することになります。メモリ安全でなくなるパターンは別途記載します

コンパイルを通るようにするコードは以下のようになります。

pub struct Repository {
    connection: RefCell<diesel::mysql::MysqlConnection>
}

impl Repository {
    pub fn new() -> Self {
        Self {
            connection: RefCell::new(MysqlConnection::establish("mysql://user:password@localhost:3306/database"))
        }
    }

    pub fn account_exists(&self, _email: &str) -> Result<bool> {
        match accounts.filter(email.eq(_email)).load::<Account>(&mut *self.connection.borrow_mut()) {
            ~ 省略 ~
        }
    }
}

構造体にはRefCell型でdiesel::mysql::MysqlConnectionジェネリクス型のメンバを定義します。コネクションタイプは適宜読み替えてください。

構造体生成初期化時にRefCell型の値を生成します。

データベースへのリクエスト時は&mut *self.connection.borrow_mut()で渡すことで Diesel の仕様変更に対応できます。

メモリ安全でないパターン

メモリ安全でなくなるパターンを下記に提示します。

pub fn account_exists(&self, _email: &str) -> Result<bool> {
    let connection = &mut *self.connection.borrow_mut();
    match accounts.filter(email.eq(_email)).load::<Account>( connection) {
        ~ 省略 ~
    }
}

メソッド部分の抜粋を一部改変したものです。ポイントはlet connection = &mut *self.connection.borrow_mut(); です。

このように別途変数を宣言してコネクションを渡してしまうと所有権がその変数に移ってしまいます。

つまり同じ構造体のインスタンス内でコネクションを使い回せなくなります。

厄介…と言う表現が正しいかどうかは分かりませんが、 RefCellによる昇格によってこの実装でもコンパイル自体は通るということです。

ですがいざ実行してこのコードの実装個所に処理が到達したとき借用エラーでPanicを引き起こすはずです。

RefCellは公式のドキュメントにも記載されていますが内部可変と呼ばれるデザインパターンです。下記にドキュメントの内容を引用します。

コンパイル時ではなく実行時に借用エラーをキャッチするということは、開発過程の遅い段階でコードのミスを発見し、 コードをプロダクションにデプロイする時まで発見しない可能性もあることを意味します。また、 コンパイル時ではなく、実行時に借用を追いかける結果として、少し実行時にパフォーマンスを犠牲にするでしょう。

一方でRefCellのデザインパターンにより他の言語で多用されているアーキテクチャにもRustで対応することが可能になります。

使い処が肝心と言うことですね!