0x90

一回休み

Iron+DieselでMVCなWebアプリを作る話

Iron単体とかでWebアプリを作る話はちょいちょい見るのですが、Iron+Dieselまで組み合わせて、、、となると意外と見かけなかったので、そのメモです。

はじめに

とりあえず、この記事では開発のベースにしやすいスケルトンを作ることを目標にします。そこで、Ironにはいくつか標準のミドルウェアがありますが、とりあえずrouterをベースにします。また、dieselに関しても(r2d2などを使って自分でmiddlewareを書いてもいいとは思いますが)こちらを使います。

Ironの構造

これまでさんざんいろいろなところで書かれているのだとは思いますが、IronはChainで必要なmiddlewareをつなぎ合わせていくという設計を取っています。例えばスタティックファイルを処理するmiddlewareがあって、動的ページを処理するmiddlewareがあって、ログを取るmiddlewareがあって、…というように、必要なmiddlewareを都度組み合わせていくスタイルです(ぼくがよく使っていた、pythonのflask, pyramidなどのフレームワークでもそうなっているのだと思いますが、ironはよりカスタマイザブルというか、この構成を理解していないとコードが全く書けない感じです。こういうところ、Rustっぽくって(?)ぼくは好きです。)。ここではまず簡単のため、ルーティング(Router)で構築したChainの例を書いてみます。大体routerのexampleからパクってきました。

extern crate iron;

#[macro_use]
extern crate router;

use iron::prelude::*;
use iron::status;
use router::Router;

fn main() {
    let router = router!(index: get "/" => handler,
                         query: get "/:query" => handler);

    let chain = Chain::new(router);

    Iron::new(chain).http("localhost:3000").unwrap();

    fn handler(req: &mut Request) -> IronResult<Response> {
        let ref query = req.extensions.get::<Router>().unwrap().find("query").unwrap_or("/");
        Ok(Response::with((status::Ok, *query)))
    }
}

Cargo.tomlにはironとrouterさえ記述しておけば大丈夫です。

Diesel部分の記述

あとはこれにDieselに対応するmiddlewareをくっつけるだけなのですが、Dieselを使う以上Diesel側のセットアップも済ませる必要があります。これそこそこ面倒くさいので、dieselのgetting startedのページを見ながらやっていきます。ただ今回はsqliteを使おうと思うので、そこだけ適当に改変しました。まずはCargo.tomlに次を書きます。

diesel = { version = "*", features = ["sqlite"] }
dotenv = "0.9.0"

また、CLI toolもインストールしていなければ入れておきます。

$ cargo install diesel_cli

dbファイルの場所はプロジェクトルートの.envファイルに書き込んでおきます。

DATABASE_URL=sqlite.db

そして、

$ diesel setup

すると、dbファイルができたのがわかると思います。次は、migrationを書いていきます。SQLAlchemyなどに慣れてしまっているとSQL書きたくない…となるのですが、migration部分では生SQLを書く必要があります。ま、逆に何やってるんだかよくわからないリフレクションの連続よりは良いという考え方もあると思いますが。

$ diesel migration generate initiate_tables
Creating migrations/2017-12-31-132020_initiate_tables/up.sql
Creating migrations/2017-12-31-132020_initiate_tables/down.sql

として、up.sql, down.sqlにそれぞれ何かを書いていきます。例えばここでは例としてusersというテーブルを作ることを考えます。これなら

-- up.sql
-- Your SQL goes here
CREATE TABLE users (
  id INTEGER PRIMARY KEY NOT NULL,
  name TEXT NOT NULL,
  email TEXT NOT NULL
)
-- down.sql
-- This file should undo anything in `up.sql`
DROP TABLE users

などとします。次に、db関連のライブラリをまとめるモジュールを作っておきます。

src/
├── db
│   ├── mod.rs
│   ├── models
│   │   ├── mod.rs
│   │   └── users.rs
│   └── schema.rs
...

みたいなディレクトリを組みます。schema.rsは、

$ diesel print-schema > ./src/db/schema.rs

とすれば自動生成可能です。これでschema.rsに生成されるコードは、ORMのクエリ関係のメソッドなどが含まれているようです。ですので、別途テーブルに対応する構造体を作る必要があります。更に、この構造体はQuery用(select, update)とInsertで分ける必要があります。(ここ、なぜ分けているのか今ひとつわかりませんでした。確かに、insertのときはヒープ上の実体(String)ではなく、参照(&'a str)で十分である、というのはわかるのですが、それを分けることを強制しなければいけないほどパフォーマンスがクリティカルになる場合があるのでしょうか?Derefとかでうまくやってくれはするみたいですが。)そこで、db/models/users.rsを作って、

use db::schema::users;

#[derive(Queryable)]
pub struct User {
  pub id: i32,
  pub name: String,
  pub email: String,
}

#[derive(Insertable)]
#[table_name="users"]
pub struct NewUser<'a> {
  pub name: &'a str,
  pub email: &'a str,
}

とか書いておきます。

middlewareの登録と利用方法

あとは、dieselのコネクションをpoolするmiddlewareをはさみます。

    dotenv().ok();
    let database = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let diesel_middleware: DieselMiddleware<diesel::SqliteConnection> = DieselMiddleware::new(&database).unwrap();

    let mut chain = Chain::new(router);
    chain.link_before(diesel_middleware);

すると、

    fn handler(req: &mut Request) -> IronResult<Response> {
        let con: DieselPooledConnection<diesel::SqliteConnection> = req.db_conn();

        use db::schema::users::dsl::*;
        use db::models::User;

        let results = users.load::<User>(&*con).expect("Error DB");

        for user in results {
          println!("{}", user.id);
        }
        
        let ref query = req.extensions.get::<Router>().unwrap().find("query").unwrap_or("/");
        Ok(Response::with((status::Ok, *query)))
    }

みたいな感じで、routerのハンドラ内でクエリを発行することができます。

Template

あと、MVCに欠かせないのはviewの部分です。Rustはいろいろテンプレートエンジンがありますが、今回はRustならではのもの、ということでaskamaを選択しました。askamaに関してはもう少し詳細な紹介記事を分けて書きたいと思いますが、エラーが大体コンパイル時にわかるので、安心感がぱないです。テンプレートの部分に関してはcontroller中で適当に入れ替えればいいので、handlebarsなど普通のテンプレートエンジンに変えるのはかんたんだと思います。

最終的なディレクトリ構成

handlerの部分を別モジュールに切り出すなどして、最終的には次のようなディレクトリ構成になりました。

iron-diesel-scaffold/
├── Cargo.toml
├── migrations
│   └── 2017-12-31-132020_initiate_tables
│       ├── down.sql
│       └── up.sql
├── sqlite.db
├── src
│   ├── controllers
│   │   ├── mod.rs
│   │   └── users.rs
│   ├── db
│   │   ├── mod.rs
│   │   ├── models
│   │   │   ├── mod.rs
│   │   │   └── users.rs
│   │   └── schema.rs
│   ├── main.rs
│   └── templates
│       └── mod.rs
└── templates
    ├── base.html
    ├── index.html
    └── list_user.html

コードはここにおいておきます。