Skip to content

8.4 - レイヤーシステムをマッピングする

レイヤーシステムをマッピングする

レイヤーシステムは、異なる機能単位を持つ層で構成されています。これらのレイヤーは異なるサーバーであると言えるかもしれません。これは、マイクロサービスや大きなシステムでも言えることです。これは、データの異なるレイヤーに関しても言えることです。ビッグシステムでは、定期的にアクセスされ更新されるホットデータと、ほとんどアクセスされないコールドデータがあることは理にかなっています。しかし、レイヤーは異なるサーバーにあると考えがちですが、同じサーバーにあることもあります。次のような図でレイヤーをマッピングすることができます。

図8.1 - アプリのレイヤーについて

ご覧のように、私たちのアプリはこの流れに沿っています。

  1. まず、HTTPハンドラーが、サーバー作成時に定義したポートをリッスンして、呼び出しを受け付けます。
  2. そして、アプリの wrap_fn 関数を使用して定義したミドルウェアを通過します。
  3. これが終わると、リクエストのURLは正しいビューにマップされ、src/json_serialization/ディレクトリで定義したスキーマにマップされます。これらは、src/viewsディレクトリに定義されているリソース(ビュー)に渡されます。

その後、データベースからデータを更新したり取得したりしたい場合は、Diesel ORMを使ってこれらのリクエストをマッピングします。この段階で、ミドルウェアを除くすべてのレイヤーは、データの流れを効果的に管理するために定義されています。前章の第7章「ユーザーセッションの管理」で指摘したように、JwToken構造体を使った認証のためのミドルウェアをFromRequest traitで実装しています。このように、wrap_fnとFromRequest traitのどちらを使ってもミドルウェアを実装することができることがわかります。wrap_fnとFromRequest traitのどちらを使うべきか、どんな時に使うべきと思いますか?どちらもメリットとデメリットがあります。特定のビューに対してミドルウェアを実装したい場合は、FromRequest traitを実装するのがベストです。これは、FromRequest traitを実装した構造体を、必要なビューにスロットすることができるからです。認証は、FromRequest traitを実装するのに適したユースケースであり、認証を必要とするエンドポイントを選択することができるからです。しかし、包括的なルールを実装したい場合は、wrap_fn関数で認証のためのビューの選択を実装する方がよいでしょう。wrap_fn関数でミドルウェアを実装することは、すべてのリクエストに対して実装されることを意味します。

例えば、すべてのエンドポイントについて、バージョン1のサポートを終了することが考えられます。この場合、サードパーティのユーザーに対して、APIのバージョン1のサポートを終了するという決定を警告する必要があります。期限が過ぎたら、バージョン1のサポートが終了したことを親切なメッセージで伝えなければならないでしょう。ミドルウェア層の作業を始める前に、main.rsファイルの先頭で次のインポートを定義する必要があります。

use actix_web::{App, HttpServer, HttpResponse};
use actix_service::Service;
use futures::future::{ok, Either};
use actix_cors::Cors;

受信したリクエストがv1エンドポイント向けであることを確認するために、リクエストを処理するか拒否するかを決定する際に、後でチェックできるフラグを定義する必要があります。これを実現するには、main.rs ファイルで次のコードを使用します。

.wrap_fn(|req, srv|{
    let passed: bool;
    if req.path().contains("/v1/") {
        passed = false;
    } else {
        passed = true;
    }
. . .

先のコードから、passedという名前でBooleanがあることを宣言していることがわかります。v1がURLに存在しない場合、trueに設定されます。v1がURLに存在する場合、passedはfalseに設定されます。

さて、フラグを定義したので、リクエストに何が起こるか指示するためにそれを使うことができます。これを行う前に、このコードブロックに示されているように、wrap_fnの最後の行に注意する必要があります。

let future = srv.call(req);
async {
    let result = fut.await?;
    Ok(result)
}

呼び出しが終わるのを待ち、その結果をresultという変数で返しています。v1 APIのブロックが呼び出されたので、リクエストが通ったかどうか確認する必要があります。通過した場合は、先のコードを実行する。しかし、リクエストが失敗した場合は、これを回避して、レスポンスだけの別のフューチャーを定義しなければなりません。

額面通りだと、これは簡単なことに思えるかもしれません。どちらも同じもの、つまりレスポンスを返します。しかし、Rustはコンパイルできません。互換性のない型に基づくエラーが投げられるのです。これは、非同期ブロックがクロージャのように動作するためです。つまり、すべての非同期ブロックはそれ自身の型であるということです。この微妙なディテールのために、開発者は2つのフューチャーを互いに連携させるために何時間も費やすことになり、イライラすることがあります。

幸いなことに、futures crateには、この問題を解決してくれるenumがあります。Either列挙型は、同じ関連型を持つ2つの異なるフューチャー、ストリーム、シンクを1つの型に統合するものです。これにより、次のようなコードで、渡されたフラグにマッチし、適切なプロセスを起動し、返すことができる。

    let end_result;
    if passed == true {
        end_result = Either::Left(srv.call(req))
    }
    else {
        let resp = HttpResponse::NotImplemented().body(
            "v1 API is no longer supported"
            );
        end_result = Either::Right(
            ok(req.into_response(resp)
                  .map_into_boxed_body())
        )
    }
    async move {
        let result = end_result.await?;
        Ok(result)
    }
}).configure(views::views_factory).wrap(cors);

先のコードから、渡されたフラグに応じて、end_resultをビューとして呼び出すか、不正なレスポンスに直接返すように割り当てていることがわかります。そして、wrap_fnの最後でこれを返します。Either列挙型の使い方を知っていると、2つの異なる未来を選択する必要があるときに、時間を節約することができ、便利なトリックです。

v1をブロックしているかどうかを確認するために、次の図のように単純なgetリクエストを呼び出すことができます。

図8.2 「ブロック化されたv1への対応

API呼び出しが親切なメッセージとともにブロックされていることがわかります。Postmanを通してAPIコールを行うと、次の図に見られるように、501 Not Implementedエラーが出ることがわかります。

図8.3 「ブロックされたv1に対するポストマンの反応

将来的にはアイテムを取得するためのリソースを増やしたいと思うかもしれません。これは、いくつかのビューがアプリのビューと衝突し始めるかもしれないので、潜在的な問題を提起します。例えば、ToDoアイテムのAPIビューはitemというプレフィックスを持つだけです。すべての項目を取得するには、v1/item/getエンドポイントが必要です。

後でv1/item/get/{id}エンドポイントで編集するために、ToDo項目を詳細に見るビューをアプリ用に開発することは合理的かもしれません。しかし、これではフロントエンドのアプリのビューとバックエンドのAPI呼び出しが衝突する危険性が高まります。これを防ぐには、APIが統一されたインターフェイスを持つようにする必要がありそうです。