shine-Notes

ゆるふわ思考ダンプ

Rust(Rocket+Diesel)でsqliteのテーブルにCRUDするだけのAPIサーバを作る

サマリ

  • Rustへの習熟がてら、フレームワーク(Rocket)を使って簡単なAPIサーバを書いた
  • 自分の普段使わない言語(Rust)で、自分の親しんでいるエリア(Webフレームワーク)を書いてみるのも勉強になる

モチベーション

今年もステイホームのGWだった。という訳で、まとまった時間じゃないと出来ないことをやりたいなーと思い、表題のタスクに挑戦したりした。モチベーションとしてはこんな感じ。

  • Rustlangの習熟。個人的にPython以外の下地を作りたい思いがあり、実践の機会を作りたかった。
  • Webフレームワークへの理解深耕。自分自身はDjango(Python)を学び始めており、どうせなら違う道具で同じ事をやろうと思った。

以上を踏まえ、今回はいずれフロントエンドは(Vueとかで)別に作ることを想定し、いわゆるAPIサーバを作ることにした。httpクライアントからのPOST,PUT,DELETEのリクエストを処理してテーブルを操作できるような初歩的なものである。もちろんフルスクラッチで書く度量はないので、ライブラリをあたる。今回は比較的ドキュメントの揃っていて(起動中のアイコンもキュートな)Rocketに入門することにした。Flaskライクでコード行数が少なく済みそうなのもよい。

rocket.rs

という訳で、やったことや詰まりどころ、所感を書いていこうと思う。いってみよう。

やったこと

Rust Rocket入門&リファレンス

この辺は公式が結構きれいにドキュメントを作ってくれている。指示通りcargo newしてCargo.tomlにRocketパッケージを追記すればさっくりHello Worldはできるだろう。

Getting Started - Rocket Programming Guide

とはいえ、ここから1からビルドアップして書いていくのは(自分には)難しい。そこで、公式が用意してくれているexampleがとても参考になる。

github.com

Rocketの作者自身が様々な用途で使えるexampleを書いてくれている(しかも結構メンテされてる)。少し内容を確認して、今回はシンプルに動作が確認できたToDoアプリをクローンして、やりたいものに近づけていくアプローチにした。

Rocket/examples/todo at master · SergioBenitez/Rocket · GitHub

ToDoアプリそのものは、フロントエンドをTeraでレンダリングして表示し、そこからTasksというModel(テーブル)をsqliteに永続化してCRUD操作できるようにしている。ここに、Tasksとは異なる独自の作りたいModel(テーブル)を作り、httpクライアントがJSONでレスポンスを受け取りながら操作できる形に書いていくことにした。

(先に書いておくと、以降の内容はGitHubにもコードを上げている(出来の如何はさておき)。以降はポイントを記載していく。)

github.com

データモデル(テーブルを作る)

まず、データモデル(いわゆるDjangoのModel)を作り、ORMを通じて永続化する部分を定義する必要がある。RDBにはsqliteを使い、ORMにはDieselを使う。単純に1テーブル作るだけなので、

  • 作りたいデータモデルに応じた構造体(Struct)を定義する
  • TableのCreateとDropSQLが入った、Migration用のup.sqldown.sqlを作る

の2ステップを踏めばよい。自分は後々起床時間を記録するようなアプリを作りたいので、テーブル名は「Records」とし、そのままテーブル操作を行う構造体を定義した。厳密な抽象化(たとえばEntityのようなドメインモデルを経由して、sqlite部分をRepositoryとして切り離すとか)はやっていない。

#[derive(Serialize, Deserialize, Queryable, Insertable, Debug, Clone)]
pub struct Record {
    pub id: Option<i32>,
    pub wakeupdatetime: String,
    pub condition: Option<i32>,
    pub description: String,
    pub isperiod: bool
}

ポイントとして、この構造体を通じてデータのシリアライズCRUDを行うので、derive(Rustにおける継承)には(Serialize, Deserialize, Queryable, Insertableを含める必要がある。その上で、この構造体CRUDに関するメソッド処理をimplで追記していき、後ほどmain.rsのルーティングの中で呼び出していくような格好になる。

impl Record {
    // id順にSELECT ALL
    pub fn all(conn: &SqliteConnection) -> Vec<Record> {
        all_records.order(records::id.desc()).load::<Record>(conn).unwrap()
    }

その他の処理については該当のrecord.rsをご参照。(※エラー処理等々実用レベルでない点にはご容赦を)

mystudying-rust-rocket-apisample/record.rs at main · shinebalance/mystudying-rust-rocket-apisample · GitHub

ルーティング(main.rs)

データモデル側での処理に対応して、main.rs側にはリクエストを受けるアドレスに対応した処理を書いていくことになる。各関数で先程作ったデータモデルの処理を呼び出すようにしておき、

// GET処理:api/v1/records/
# [get("/")]
fn api_records(conn: DbConn) -> Json<Vec<Record>> {
    Json(
        Record::all(&conn)
    )
}

rocket().launch()から実行されるrocketのインスタンスにURLと対応する関数をマウントしておくことで、サーバ実行時のルーティングが行われる。このへんはFlask(Python)に近い書き味でシンプルなのがありがたい。

// main実行した時に走るやつ
fn rocket() -> Rocket {
    rocket::ignite()
    .attach(DbConn::fairing())
    .attach(AdHoc::on_attach("DatabaseMigrations", run_db_migrations))
    // 中略
    .mount("/api/v1/records",routes![api_records]) //GitHub上のコードでは他にもルーティングしているが、ここでは省略
}

あとは都度都度cargo buildしたりcargo runしたりしながら挙動をチェック。

f:id:shinebalance:20210509185231p:plain

こんな感じ。 こちらも、その他の処理については該当のmain.rsをご参照。

mystudying-rust-rocket-apisample/main.rs at main · shinebalance/mystudying-rust-rocket-apisample · GitHub

テストコード

今回は元になったexampleにテストコードも有ったので、見様見真似で書いてみることにした。cargo testで実行できる。おなじみのアサーションもあるし、Rocket自体にhttpクライアントを実行できる機能があるので、これを使うと簡単なテストは書けそうな感じ。

#[test]
fn test_get_record() {
    run_test!(|client, conn| {
        // レコード数のチェック
        let init_records = Record::all(&conn);
        // Getできることの確認
        let _req = client.get("/api/v1/records");
        // id:1を使って単体レコードの取得テスト
        let check_id = 1;
        let retrieved_records = Record::retrieve_by_id(check_id, &conn).unwrap();
        if retrieved_records.len() != 0 {
            // get処理
            let url = format!("/api/v1/records/{}", check_id);
            client.get(url);
            assert_eq!(retrieved_records.len(), 1);
        }
    })
}

振り返り

実用レベルとは言い難いが、以上の繰り返しで何とかそれっぽく動くAPIサーバは作ることが出来た。振り返りとして、何点か。

Rocketの使い勝手

印象だが、ORMとしての機能はDieselに頼っているので、シンプルにルーティング~シリアライズを受け持ってくれる…まさにRust版Flaskという前評判通りだった。見ての通りコード量が少なくて済むのでプロトタイプにはもってこいという印象。

Rustの使い勝手

これは大いに個人的な感想だが、ふだんPythonを書いている身からすると、Pythonが抽象化してくれていた世界から1つ泥臭いレイヤに戻る感じが面白い。それでいて、ちょくちょく新しいアイデアが吸収されている。そして何と言ってもcargoが賢い。rls(Rust Language Server)も入れておけば大体間違いを正してくれる。

Rustのことがまだまだわからない(ex.Options型, デバッグ手段)

本記事で回答のない話なので、自分の宿題メモと言った雰囲気にはなるが…

  • Options型が上手く使いこなせない。上手に使えば良い安全弁になりそうというのは分かるのだが……まだ使いこなせている感じがしない。もう少し勉強か。
  • いわゆる思考停止のprint文デバッグができなかったので、結構苦労した。いいやり方は有る気がする。

Rustは参考書は以下を読んでいる最中で、他にも数冊積んでいるものが有るので、気長に取り組んでいこうとは思う。

プログラミング言語Rust入門

プログラミング言語Rust入門

所感

とはいえ個人的には参考書レベルでしか理解していなかったRustの文法を実際に使うところ、他の重量級?フレームワーク(Django)を通ったあとに軽量フレームワークを使う事での気付きだったりが得られて楽しかった。個人的にPythonだけでソフトウェア開発を学び続ける事に限界を感じても居たので、実践レベルは数年先くらいの気長な学習のつもりで、Rustは折に触れて使っていこうと思っている。