Option.mapの中でResult型を返したい

業務でRustを書いている時に受けた質問を元に一記事書きます。

Rustを書いているとたまにイテレータのmapメソッドの中でResult型の値を返したい時があります。

例えば以下のようなコード

fn i32_to_u16(x: Option<i32>) -> anyhow::Result<Option<u16>> {
    x.map(|x| x.try_into().with_context(|| "変換エラー"))
}

このコード自体はコンパイルエラーになりますがやりたいことは分かるかと思います。

i32型の取り得る値は-2,147,483,648~2,147,483,647の範囲、u16型の取り得る値は0~65,535の範囲です。

このため i32-> u16の変換ではエラーが発生する可能性があり、変換には .try_intoを用いる必要があります。

しかしこの時 .mapメソッドの返す値の型は Option<Result<u16>>となり、求めている Result<Option<u16>>とはなりません。

x.try_into()が返すのは Result<u16>型なので x.map(|x| x.try_into()?)というように ?を使ってunwrapしてやれば良いと思うのですが、これも下記のようなエラーとなるためできません。

?オペレータをクロージャの中で使用する時は、クロージャResult型あるいはOption型を返さないといけないというものです。

error[E0277]: the `?` operator can only be used in a closure that returns `Result` or `Option` (or another type that implements `FromResidual`)
   |
18 |     x.map(|x| x.try_into()?)
   |           ---             ^ cannot use the `?` operator in a closure that returns `u16`
   |           |
   |           this function should return `Result` or `Option` to accept `?`
   |
   = help: the trait `FromResidual<Result<Infallible, TryFromIntError>>` is not implemented for `u16`

解決策

Option<Result<T>>型を Result<Option<T>>に、あるいは Result<Option<T>>型を Option<Result<T>>型に変換するための .transposeメソッドを使用ことでこの問題が解決できます

このメソッドを使うと冒頭のコードは以下のように書くことができます。

fn i32_to_u16(x: Option<i32>) -> anyhow::Result<Option<u16>> {
    x.map(|x| x.try_into().with_context(|| "変換エラー")).transpose()
}

この辺りのことはresultモジュールあるいはoptionモジュールのドキュメントに書いてあります

https://doc.rust-lang.org/std/result/

https://doc.rust-lang.org/std/option/

同じようなメソッドとして Option<Option<T>>Option<T>に、あるいは Result<Result<T>>Result<T>に変換するための .flattenと言うメソッドもありますね。

Rust製のWebアプリケーションのトランザクション管理方法について

業務でRustでWebアプリケーションを書いています。 同然データベーストランザクションを扱っているのですが、トランザクションの管理方法が煩雑で不具合になったりしていました。

これについて解決方法を考えてみたのでブログ記事として残してみます。

前提

使用しているクレートは以下のものになります。

Webアプリの構成について

アーキテクチャは所謂クリーンアーキテクチャを採用しています。 上の階層から順に列挙すると、

  • handler (HTTPクリエスト、レスポンスと言った部分を管理する層)
  • usecase (業務ロジックを管理する層)
  • repository (データベースアクセスや外部APIとの通信を管理する層)

のようになっています。

1つのリクエストの処理に沿って見てみると、

  1. handlerがHTTPリクエストを受け取る
  2. usecaseが業務ロジックに基づき処理を行う
  3. usecaseからrepositoryの実装を呼び出してDBアクセスなどを行う
  4. repositoryの処理の結果をusecaseへ返す
  5. usecaseが業務ロジックに基づき処理を行う
  6. handlerがHTTPレスポンスを返す

と言った流れです。

これまでのトランザクション管理方法

クリーンアーキテクチャを採用したWebアプリケーションでDBコネクションを管理しようと思うと、 repositoryにコネクションを持つのが一般的ではないかなと思います。 以下のような形です。

use sea_orm::DatabaseConnection;

struct DatabaseRepositoryImpl {
  db_conneciton: DatabaseConnection,
}

ただし、コネクションをrepositoryに持たせるとrepositoryの複数の処理に跨がるようなトランザクションを開始出来ないという問題があります。

例えばrepositoryが以下のように実装されているとします。

impl DatabaseRespository for DatabaseRepositoryImpl {

  pub async fn create_user(&self, user: User) -> anyhow::Result<()> {
    let tx = self.db_connection.begin();
    // ユーザ作成処理 (省略)
   tx.commit().await
  }

  pub async fn create_blog(&self) -> anyhow::Result<()> {
    let tx = self.db_connection.begin();
    // ブログ作成処理 (省略)
   tx.commit().await
  }
}

これをusecase層で以下のように使うことを考えた時に、create_usercreate_blogはそれぞれ独立したトランザクション毎の処理となります。

pub fn create_user_and_blog(&self, user: User) -> anyhow::Result<()> {
  self.database_repository.create_user(user).await?;
  self.database_repository.create_blog().await
}

なので場合によっては、userは無事作成されたものの、blogは作成できなかったという状況に陥ります。

これを回避するために、DBコネクションはactix-webのデータとして持つ方法を採用しています。 リクエストの度にhandlerの引数としてDBコネクションを含むデータを受け取り、それをusecaseに渡す。 usecase内でトランザクションの開始をして、処理の最後でコミットあるいはロールバックをするという方式です。

ソースコードを見ていきます。

まず、Webアプリの起動時にDBコネクションを確立します。それをactix-webのapp_data()に渡していました。

use sea_orm::DatabaseConnection;

#[derive(Clone)]
pub struct AppState {
    db_conn: DatabaseConnection,
}

impl AppState {
    pub fn new(db_conn: DatabaseConnection) -> Self {
        Self {
            db_conn,
        }
    }

    pub fn db_connection(&self) -> DatabaseConnection {
        self.db_conn.clone()
    }
}


async fn main() -> std::io::Result<()> {
let db_connection = database::establish_database_connection(config.db_dsn())
        .await
        .unwrap();

    let app_state = AppState::new(db_connection);

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(app_state.clone()))
            .service(
                web::scope("/protect")
                    .service(post_account)
            )
            .service(web::scope("/public"))
    })
    .bind(("0.0.0.0", 9090))?
    .run()
    .await
}

async fn post_account(ctx: web::Data<AppState>, body: web::Json<PostAccountRequest>) -> AppResult {
    let account =  self.get_usecase()?
        .create_user_and_blog(ctx.into_inner(), body.into_inner().into())
        .await?;
    Ok(HttpResponse::Ok().finish())
}

App::new()のタイミングで.app_data()にDBコネクションを含む構造体AppStateを渡しています。

その後リクエストを受け取った時に、post_accountの引数の1つとしてAppStateを受け取り、usecase層のメソッドに渡しています。

ではusecase層はこれをどのように使うのかというと、以下のようになります。

pub fn create_user_and_blog(&self, ctx: AppState, user: User) -> anyhow::Result<()> {
  let tx = ctx.db_connection().begin().await?;
  self.database_repository.create_user(&tx, user).await?;
  self.database_repository.create_blog(&tx).await?;
  tx.commit().await
}

冒頭でトランザクションを開始し、トランザクションをrepository層の各メソッドに渡しています。 repository層では受け取ったトランザクションを用いて処理を行い、全ての処理が完了するとusecase層の最後にコミットされるという流れです。

このようにすることにより、respoitoryの各処理が同一のトランザクション内で実行されるため、 userが作成されたけど、blogは作成されなかったという状況を防ぐことができます。

問題点

この方法の問題点として、usecase層の処理の最後でコミットすることを忘れるという問題がありました。

最後のコミットを忘れるとどうなるのかと言うと、変数txdropされたタイミングでトランザクションロールバックし、 変更が全てなかったことになります。

当然テストを実施すればバグとして問題には気付けるのですが、逆に言えばテストをするまで気付かないことも多いです。

解決方法

usecase内の処理の成否によって暗黙にコミットあるいはロールバックできれば、この問題は解決しそうです。

そこでトランザクションを管理するための構造体を用意してみました。

use anyhow::Context as _;
use sea_orm::{
    ConnectOptions, Database, DatabaseConnection, DatabaseTransaction, TransactionTrait,
};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;

#[derive(Clone)]
pub struct TransactionManager {
    database_connection: DatabaseConnection,
}

impl TransactionManager {
    pub fn new(connection: DatabaseConnection) -> Self {
        Self {
            database_connection: connection,
        }
    }
}

impl TransactionManager {
    pub async fn run_in_transaction<T, F>(&self, f: F) -> anyhow::Result<T>
    where
        T: Send,
        F: FnOnce(
                CloneableDatabaseTransaction,
            ) -> Pin<Box<dyn Future<Output = anyhow::Result<T>> + Send>>
            + Send,
    {
        let tx = self.database_connection.begin().await?;
        let cloneable_tx = CloneableDatabaseTransaction::new(tx);
        let tx_clone = cloneable_tx.clone();

        let result = f(cloneable_tx).await;

        match result {
            Ok(_) => tx_clone.commit().await?,
            Err(_) => tx_clone.rollback().await?,
        }

        result
    }
}

フィールドとしてDBコネクションを持ち、また特定の処理をトランザクション内で処理するためのメソッドrun_in_transactionを持ちます。

run_in_transactionの内部ではまずトランザクションを開始し、そのトランザクションをクローンします。 クローンは後にコミットあるいはロールバックを実施するために必須です。

クローンされたトランザクションを引数で受け取ったクロージャに渡し、クロージャ内では受け取ったトランザクションを使ってDBアクセスを行います。 クロージャの処理が完了し、結果が戻ってくると、その結果を元にコミットあるいはロールバックを行います。

これを行うためにsea_rom::DatabaseTransactionのラッパーとしてCloneableDatabaseTransactionを用意しています。

実装は以下のようになっています。

#[derive(Clone)]
pub struct CloneableDatabaseTransaction(Arc<DatabaseTransaction>);

impl CloneableDatabaseTransaction {
    pub fn new(tx: DatabaseTransaction) -> Self {
        Self(Arc::new(tx))
    }

    pub async fn commit(self) -> anyhow::Result<()> {
        let tx = Arc::try_unwrap(self.0).map_err(|_| anyhow::anyhow!("Cannot unwrap Arc"))?;
        tx.commit().await.map_err(Into::into)
    }

    pub async fn rollback(self) -> anyhow::Result<()> {
        let tx = Arc::try_unwrap(self.0).map_err(|_| anyhow::anyhow!("Cannot unwrap Arc"))?;
        tx.rollback().await.map_err(Into::into)
    }

    pub fn inner(&self) -> &DatabaseTransaction {
        &self.0
    }
}

sea_orm::DatabaseBaseTransactionArcで囲んでいます。これはトランザクションがスレッド安全であることを求められるためです。 またトランザクションがクローン可能であることを求められるため、Cloneを実装したラッパーを用意しました。

これらの実装を用いるとusecase内で明示的にコミット・ロールバックすることを回避できます。

まず、AppStateTransactionManagerを持つことができるように修正します。

use crate::database::TransactionManager;

#[derive(Clone)]
pub struct AppState {
    transaction_manager: TransactionManager,
}

impl AppState {
    pub fn new(transaction_manager: TransactionManager) -> Self {
        Self {
            transaction_manager,
        }
    }

    pub fn tx_manager(&self) -> TransactionManager {
        self.transaction_manager.clone()
    }
}

この時点でサーバの初期化処理で型エラーが発生するので、そちらも適宜修正します。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let db_connection = database::establish_database_connection(config.db_dsn())
        .await
        .map_err(|_| std::io::ErrorKind::Other)?;

    let tx_manager = database::TransactionManager::new(db_connection);
    let app_state = AppState::new(tx_manager);

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(app_state.clone()))
            .service(
                web::scope("/protect")
                    .service(post_account)
            )
            .service(web::scope("/public"))
    })
    .bind(("0.0.0.0", 9090))?
    .run()
    .await
}

するとusecase層では処理を以下の様に書くことができます。

pub fn create_user_and_blog(&self, ctx: AppState, user: User) -> anyhow::Result<()> {
  // repositoryはCopyトレイトを実装している必要がある
  // あるいはCloneトレイトを実装して、let repository = self.database_repository.clone()
  let repository = self.database_repository;

  ctx.transaction_manager()
    .run_in_transaction(|tx| {
       Box::pin(async move {
            database_repository.create_user(&tx, user).await?;
            database_repository.create_blog(&tx).await?;
            Ok(())
          })
        })
    }).await
}

処理をrun_in_transactionに与えたクロージャ内で行うことで、単一のトランザクションでrepository層の処理が実行されます。 またクロージャ内の処理の成否によって暗黙にトランザクションがコミット・ロールバックされるようになりました。

これによりusecase層の処理の最後にコミット処理を記述する必要がなくなり、 うっかりバグが発生してしまうことがなくなりました。

async moveによりrepositoryの所有権がクロージャ内へ移ってしまうため、予めコピー(またはクローン)する必要があるところもポイントです。

まとめ

Rustで作ったWebアプリでトランザクションの管理を行う方法を変更しました。

変更前と変更後のどちらが良いのかは人によるのかなと思います。

run_in_transactionの使用時にクロージャBox::pin(async move {})で囲む必要があり、これを忘れそう(面倒くさい)という人もいるかと思います。

ですが、Box::pin(async move {})で囲うのを忘れた場合はコンパイルエラーとなります。 一方で元の方法でtx.commit()を忘れた場合はコンパイルエラーにはなりません(なので不具合に繋がる)。

個人的にはプログラマのミスをコンパイルが発見してくれる変更後の方法の方が、よりRustらしい書き方だと思います。

tmux利用時にPATHが重複する問題

普段ターミナルエミュレータを利用する際は必ずと言っていいほどtmuxを使っている。
複数のシェルを横に並べたりタブに分けられる上にショートカットで自在にカーソル移動が出来て便利。

そんな便利なtmuxだが、利用時に環境変数PATHの値が一部重複していることに気がついた。
tmuxを利用し始めて数年経つのに何故今更。

tmuxを利用する時の手順は以下のようなフローになる。

1.ターミナルエミュレータを起動(zshが起動し、.zshrcを読み込む)
2.tmuxを起動(再びzshが起動し、.zshrcを読み込む)

.zshrcが2回読み込まれていることが分かる。
.zshrcの記述を見てみると自分の場合はPATHの設定に以下のような記述をしていた。

PATH=(任意のディレクトリ)/bin:PATH

既存のパスの先頭に追加したいディレクトリのパスを結合する形。
ここまで書けば問題の原因は明白で、
2度の.zshrcの読み込みによって同じティレクトリへのパス追加が2度起こっていることである。

解決方法としては以下の様にすれば良い。

if [[ -z $TMUX ]]; then
    PATH=(任意のディレクトリ)/bin:PATH
fi

環境変数TMUXでtmuxの利用の有無を判定し、利用している場合はパスの結合を行わない。
これで2度目のパスの結合は行われずに済む。

最近、ブログが停滞気味なのでザッと書いた。

追記

調べたところ

typeset -U PATH

上記の様にするだけで重複したPATHを自動的に絞り込むように環境変数の動作が変わるらしい。
こっちの方が合理的。
if文判定を用いた場合は、仮にPATHをzshrcファイルの複数の場所で結合している場合に、
その数だけif文を書かなくてはいけないので不便。

RustでOSを自作する (1)カーネルの呼び出しまで

数年前に「30日ででできる! OS自作入門」を読みながらOSを作ったことがあった。
当時はふむふむと思い一通りOSができたことでOSを理解していたつもりになっていたが、
最近になって「OSよくわからん」みたいな気分に再びなってきた。
また当時はgitのようなVCSも使っていなかったのもあり、書いたコードが残っていない。
よって再びOSを作ることで今度こそ本当にOSを理解していく。

multiboot header

アセンブラで1st stage loader、2nd stage loaderとガリガリ書いていくのはさすがに面倒なのでローダはgrubを使用する。
まずはmultiboot headerの設定から。
以下を参考にした。
http://nongnu.askapache.com/grub/phcoder/multiboot.pdf

最低限必要なのは、magic、architecture、header size、checksum、tagぐらい。
この辺はただ単にざっと並べて行けば良い。

gist89ab43b8a6a05adeab304d4d8128892e

カーネル(アセンブラ部分)

つづいて、さっそくカーネルを書いていく。
やっていることは単純で外部ファイルで定義されているはずのkernel_main関数を呼び出している。
このkernel_main関数は後ほどRustで書いていく。

gist810e038da8daf292d8a50fe530a58597

続いてMakefileを書く。以下の通り。

gist2582f090fc0b6ca15aebcf179a702412

今回のOSは32bitで作っていきたいため、nasmのオプションでアウトプットとして32bitのELFフォーマットを指定している。
これでmakeコマンドを叩けば*.oというバイナリファイルができあがる。

bootという名前でディレクトリを作り、以上のコードをまとめて入れておく。

カーネル(Rust部分)

OSを書く場合はC言語が使われることが多いが個人的な趣味の問題でRustで書いていく。
早速Rustのプロジェクトを作成

cargo new --lib kernel

続いてプロジェクトの情報を指定するファイルを作成。
1つはおなじみのCargo.toml、もう一つはRustが実行されるOSの情報を記述するJSONファイル。
後者は通常のRustプロジェクトでは必要ないものだが、
自作OSのようなRustのtargetとしてサポートされていないような環境への実行ファイルを作るために必要になる。
まずはCargo.tomlから。

gist80b0ea193d9702d11d4bcaf36a3414a2

自作OSではライブラリの動的リンクはできない(そもそも環境上にライブラリが存在しない)ため静的なオブジェクトを作る必要がある。
そのためcrate-typeでstaticlibを指定している。

続いて環境情報を記述したJSONファイル。

gistf95a79c9739a495d7a58e1a54fa440be

実際これはx86アーキテクチャ向けの情報とほとんど変わらないのだが、OSは既存のものではないため"none"としている。

以上、2つのファイルが出来たらいよいよカーネル本体へ。

gisteff30cbbebb05721dc283331e2052400

先程boot.Sでexternしたkernel_main関数を定義している。
内部で行っていることは至極単純で、
メモリの0xb8000から始まるVGA text bufferに値を書き込むことで画面上に文字列「SawayakanaAsa」を表示している。
またpanic関数はRustがPanic状態に陥った時に呼び出される関数で定義していないとコンパイルエラーになる。
現状は内部で特になにもしていない。

続いてMakefile

gistc89015334427d17b931ad33725442d8b

指定したtarget向けのバイナリファイルが出力するため、コンパイルにはXargoを使う。
github.com

リンカ

以上でOSの起動までに必要なコードは揃った。
最後にリンクを行えば見事OSが起動する。
ldscriptという名前でディレクトリを作り、その下にリンカスクリプトを配置する。
リンカスクリプトは以下の通り。

gistdc66edea7ba38b00610158a08c011e4e

メモリの1M(=0x1000000)以降に.multiboot_headerセクションを、続いてboot.SとRustで書いたカーネル部分に含まれるであろう.textセクションを展開している。

GRUB

今回はブートローダーにGRUBを使うため、その設定ファイルも必要。

gist5e526a3c57abadb73ccaf4177f0c5fd7

また、最終的に全てのビルド作業をmakeコマンドで一括実行したいのでMakefileを作る。

gist88d24ac704cfa7ef33fd972f3db5141b

ディレクトリ構造

ここまで作成したファイルを以下の様に配置する。

.
├── boot
│   ├── boot.S
│   ├── Makefile
│   └── multiboot_header.S
├── grub.cfg
├── kernel
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── i386-sawayakanaos.json
│   ├── Makefile
│   └── src
│       └── lib.rs
├── ldscript
│   └── linker.ld
└── Makefile

あとは1番上のディレクトリでmakeコマンドを叩けば、buildディレクトリが作成され、その直下にSawayakanaOS.isoが作成されているはず。
これをQEMUで実行してみる。

make run

f:id:itto-ki:20180805165710p:plain

OSが立ち上がり、画面上に「SawayakanaAsa」と表示される。めでたい。

注意点としてgrub-pc-binをインストールしないとmake内でgrub-mkrescueした際にEFIイメージが作成される。
今回はGRUBイメージを作成したいのでこれをインストールすること。

雑記

Rustで書いたカーネルのファイルサイズがかなりでかい。
既に752Kbyteもある。

github.com

AtCoderでテストを簡単に行うツールを作成した

少し前から競技プログラミングに興味を持ち、AtCoderの過去問を解いている。
AtCoderの過去問は以下のサイトにまとめられている。
AtCoder Problems

解答プログラムのテストについて

解答プログラムができたらテストを行いたいと考える。
AtCoderのサイトでは、各問題に対して入力値と出力値のサンプルが提供されている。
テストを行いたい場合は、この入力値を作成した解答プログラムに与え、
プログラムの出力値とサイトに書いてある出力値と比較することで作成した解答プログラムの正しさを確認できる。
しかしながら、このテストを行う流れが割と面倒くさいという問題がある。
普段、自分はプログラムをターミナルエミュレータの上で書いて、更にその上でコンパイル、テスト実行を行っているのだが、
このやり方ではブラウザとターミナルエミュレータをいちいち切り替える必要があり手間が掛かる。
できれば全てをターミナルエミュレータ上で行いたい。
というわけでテストをコマンド1つで行えるツールを自作した。
似たようなツールはいくつか存在しているのだが、
個人的にあまり馴染めなかったため車輪の再発明を行ってしまった。

Atarg

ツールはAtargと名付けた。特に意味はない。
github.com

Pythonモジュールとして提供しているため、以下のコマンドで簡単に導入できる。

pip3 install atarg

使い方は簡単で以下通り。

usage: atarg [-h]
             {ABC,ARC,AGC} contest_no {A,B,C,D}
             command [command ...]]

具体例を挙げる。
Beginner Contestの018の問題Aをテストしたい場合はこうだ。

atarg ABC 018 A ./A

ここではカレントディレクトリにある実行ファイルAが解答プログラムとしている。
このコマンドを叩くと、Atargは問題の掲載されているWebページからサンプルの入力値と出力値を取ってくる。
その後、入力値を実行ファイルの標準入力へ与え、解答プログラムの出力値をWebページから取ってきた出力値と比較する。

個人的な使用法

これでもまだ問題はあって、Atargは与えるべき引数が多いため面倒くさい。
自分はAtCoderのプログラムを置くディレクトリを、例えばABC007であったら$HOME/Works/atcoder/ABC/007としている。
また解答プログラムについてはRustで作成しており、A.rsのようなファイル名を付けているため、rustcでコンパイルするとAという実行ファイルが出来上がる。
なのでatcというファイル名で以下のようなシェルスクリプトを書いて$HOME/.local/binに置いている。

gistdd42730cf48bf9be9a189eb2aa639d41
やっていることは簡単で、ディレクトリのパスにコンテスト名ABCと番号007が含まれているので、
切り出してAtargに与えている。
このスクリプトを利用することにより、Atargをそのまま使用するよりも簡潔に以下のようなコマンドでテストが行える。

atc A

これで明示的には解くべき問題(タスク)名だけを与えてやればよくなった。
これならタイプ量が減りより楽である。

課題

取り敢えずテストはできるようになったので、プログラムの提出も行えるようにしたい。
また、実行したプログラムが使用した時間とメモリ量も出力できるようにしたい。
これは作者自身のレベルが低く、まだ時間とメモリ量を意識するような問題を解いていないため後回しになる可能性がある。
更に困難な課題として、解答が複数個あるタイプの問題(例えば、ABC006のC問題など)には対応できていない。
上2つの課題がやるだけなのに対して、この問題については具体的な解決法が思いつかない。

まとめ

AtCoderでテストを簡単に行えるようにするプログラムを作成した。

React+Express+Node.js+MongoDBでブログっぽい何かを作る (2)React編前半

前回はExpressでAPIサーバを作った。
itto-ki.hatenablog.com

今回はReact+Reduxでフロントエンドを作っていく。
ただ、フロントエンドはやたらとコード量が増えたので、
ReactやReduxのtutorialを見れば分かるようなことには触れず、要点だけをまとめていく。

index.js

まずはエントリポイントとなるfrontend/project/src/index.jsについて。

gistced49142b1051759a64602b4a88d60b3

今回は非同期処理にredux-saga、ページ遷移処理にconnected-react-routerを使用している。
これらの仕様にはMiddlewareを用意してやる必要があるで、それを行っているのが13~15目。
ここで作ったMiddlewareを17~25行目でstoreに入れている。
複数のMiddlewareをstoreに渡す際にはapplyMiddlewareでまとめられる。

connected-react-router

今回のプロジェクトではルーティング処理にconncted-react-routerを使用した。
reactを補助するルーティングライブラリにはreact-router, react-router-reduxそしてconnected-react-routerと種類が豊富にあり、
どれを使用するのが良いのか非常に悩まされた。
様々な技術記事を読むとreact-router-reduxを使用している場合が多いみたいだが、サンプル通りコーディングしてもまともに動作しなかった。
α版だからかもしれないが、原因は現在調査中。


gist8a2c3c2ffc1b51ae382e9e1a297bc2de

今回はアクセスされるURLパスに応じて、それぞれ'/'ならArticleListに, '/new'ならCreatorコンポーネントに振り分けている。
ArticleListはトップページとなるもので、記事の一覧が表示される。
Creatorはその名の通り、新しい記事を作成するためのもの。
上記のコードの様に、Switchタグの外にTopBarタグを記述することによって、
TopBarコンポーネントはいずれのURLパスであっても表示される。
ただし、ConnectedRouterタグはchildren elementとして1つしかelementを持てないため、
divタグで全体を囲う必要がある。

非同期処理

これが今回のコードを書いていく上で1番感動した。
redux-sagaを使用すると非同期処理が同期処理の様に記述できる。
こんな感じに。

gist3ff31487571ea48a5a99a7ef4e839ef5

非同期処理として呼び出される関数がジェネレーター関数として定義されている。
50~54行目でジェネレーター関数と対応するaction typeを結びつけていて、
actionが発生するとそれに対応するジェネレーター関数が別スレッドで実行される。
このように記述できるおかげで、同期処理の様に書けることはもちろん、
シンタックスの上で同期処理とは独立しているように書けるためコードが簡潔になる。

まとめ

ブログっぽい何かについて、記事一覧と記事作成機能を作成した。
要点は2つで、ルーティング処理と非同期処理。
それぞれconnected-react-routerとredux-sagaを使用することで簡潔に記述することが可能になる。

ここまででとりあえずはブログっぽいものが動くようになった(ブログよりTodoリストに近いが)。
コードは以下にある。
github.com

ここまでのコードはv0.1タグとしてまとめてある。

React+Express+Node.js+MongoDBでブログっぽい何かを作る (2)Express編

前回はDocker上で環境の構築を行った。
詳細は以下の記事の通り。
itto-ki.hatenablog.com

今回はExpressフレームワークAPIサーバを作っていく。

API設計

純化のため、ユーザ認証機能などは省く。
記事一覧と単体の記事に関するAPIさえあれば良い。
よってAPIは以下の通りになる。

/articles/ GET 記事一覧取得
/articles/ POST 記事作成
/articles/:id GET idで指定した記事を取得
/articles/:id PUT idで指定した記事を更新
/articles/:id DELETE idで指定した記事を削除

スキーマ定義

まずは記事のスキーマを定義する。
MongoDBとの通信にはmongooseを用いるため、
backend/project/db/articleModel.jsに以下の様に書く。

gista61bca482b6fdd5c5602164d9d56c378

記事のタイトルと本文、作成日と更新日をそれぞれ定義している。
作成日と更新日にはデフォルトの値として現在の日時を指定している。

API処理

続いてAPI呼び出し時の処理を書いていく。
ファイル分割のため、backend/project/routes/articles.jsに処理を書き、/backend/project/app.jsから読み込むという流れを取る。
backend/project/routes/articles.jsは以下の通り。

gist1b2b67b340ccac544c9900a57235e7c6

routerインスタンスを作り、それぞれGETをget、POSTをpost...というように各HTTP リクエストメソッドに対するrouterインスタンスのメソッドを定義する。
例えば/articles/に対するGETメソッドに対する処理は5行目から始まるrouter.getメソッドで定義される。
なお、ここでは/articles/に対する処理にも関わらず、URLとの対応を指定するgetメソッドの第一引数は'/'となっている。
これを/articlesに対応させる処理は/backend/project/app.jsからbackend/project/routes/articles.jsを読み込む時に行う。

データベースへの接続処理とarticles.jsのインポート

上記の/backend/project/routes/articles.jsを作成したら、/backend/project/app.jsの中でインポートする。

gist3368c7cbb0658a1500fc5561db9e7c1c

9行目で先程作成した/backend/project/routes/articles.jsをインポートしている。
そして32行目で/backend/project/routes/articles.jsで定義した各メソッドを/articlesのURLと対応付けている。
例えば先程'/'と対応付けたgetメソッドは/aritclesのプレフィックスが付き、
URL /articles/にGETリクエストメソッドを送った場合に実行されることになる。
その他、このファイルで重要な点としては、以下の2点が挙げられる。

  • 14行目でデータベースへとコネクションを張っている
  • 27行目でオリジン間リソース共有をどのドメインからも許可する設定としている

まとめ

以上で簡単なAPIサーバが完成した。
Postmanやcurlを使って対応するHTTPリクエストを投げてみると正しく動く事が確認できるだろう。

次はいよいよReactでフロントエンドを作っていく。