規約縛りのRESTful API

LastaでのRESTful API

LastaFlute-1.2.1 よりサポートしています。

※合わせて、LastaMeta-0.5.1, UTFlute-0.9.3 以上を利用しましょう

ざっくり概要

@RestfulActionアノテーション付けて、LastaFluteの規約に沿ってメソッド定義していきます。

前提となる設定
ActionAdjustmentProviderでRestfulRouterを設定
Action
Actionクラスに@RestfulActionアノテーションを付ける
HTTP Method
Executeメソッドにget$index()とHTTP Methodを付ける
リストと一件
リストと一件だけはOverloadメソッドで、どちらもget$index()
ネストリソース
リソース名をクラス名でつなげて、パスパラメーターは順番で引数に
ハイフンつなぎ
@RestfulActionのhyphenate属性で指定
EventSuffix
@RestfulActionのallowEventSuffix=trueにしてget$sea()
HTTP Status
規約ベースやアノテーション属性指定などいろいろ

ざっくりExample

ひとまず、Actionクラスはこのような感じになります。

Exampleプロジェクトである lastaflute-example-maihamaサービスの maihama-showbaseアプリにてExampleがありますので、そちらもご覧ください。

e.g. RESTful action for /products/ @Java
@RestfulAction
public class ProductsAction extends ShowbaseBaseAction {

    @Execute
    public JsonResponse<List<ProductsResult>> get$index(ProductsSearchForm form) {
    ...
    @Execute
    public JsonResponse<ProductsResult> get$index(Integer productId) {
    ...
    @Execute
    public JsonResponse<Void> post$index(ProductsPostBody body) {
    ...

RESTful API の Action

RestfulRouterが設定されていることが前提です。(デフォルト設定では動作しません)

@RestfulActionを付与

Actionには、@RestfulActionアノテーションを付けます。

e.g. action with @RestfulAction annotation for /products/ @Java
@RestfulAction
public class ProductsAction extends ShowbaseBaseAction {

@ExecuteメソッドにはHTTP Methodを付与

メソッド名の先頭で HTTP Method を表現します。小文字で付けて $ (だらー) で区切ります。

$以降の業務的なメソッド名は(基本的に)index固定です。(ただし特殊利用方法あり、後述)

e.g. action execute with HTTP method for /products/ @Java
@Execute
public JsonResponse<ProductsResult> get$index(Integer productId) {
...
@Execute
public JsonResponse<Void> post$index(ProductsPostBody body) {
...

リストと一件だけはOverloadメソッドで

通常LastaFluteの@ExecuteメソッドではOverloadはできませんが、RESTfulのリスト検索と一件検索のメソッドだけは、Overloadができます。 つまり、get$index()を二つ定義することができます。

e.g. you can use overload method for get$index() /products/ @Java
@Execute
public JsonResponse<List<ProductsResult>> get$index(ProductsSearchForm form) {
...
@Execute
public JsonResponse<ProductsResult> get$index(Integer productId) {
...

ネストリソースは? e.g. /products/1/purchases/2/

ネストリソースのときは、リソース名の途中に挟み込まれるパスパラメーターを気にせず、リソース名でクラス名を構築します。 そして、パスパラメーターは引数で左から順番に定義していきます。

Actionクラス名
ProductsPurchasesAction
Executeメソッド
get$index(Integer productId, Long purchaseId)
e.g. you can handle nest resources /products/1/purchases/2/ @Java
@RestfulAction
public class ProductsPurchasesAction extends ShowbaseBaseAction {

    @Execute
    public JsonResponse<List<ProductsPurchasesResult>>
            get$index(Integer productId, ProductsPurchasesSearchForm form) {
    ...
    @Execute
    public JsonResponse<ProductsPurchasesResult>
            get$index(Integer productId, Long purchasesId) {
    ...

つまり、通常のActionでの /products/purchases/1/2/ に相当する定義をします。

ActionのExampleコード

ルートリソースのExample e.g. /products/

例えば、/products/ というリソースがひとつの場合のActionの実装Exampleです。

e.g. action for root resource e.g. /products/ @Java
@RestfulAction
public class ProductsAction extends ShowbaseBaseAction {

    // ===================================================================================
    //                                                                           Attribute
    //                                                                           =========
    @Resource
    private ProductsCrudAssist productsCrudAssist;
    @Resource
    private ProductsMappingAssist productsMappingAssist;

    // ===================================================================================
    //                                                                             Execute
    //                                                                             =======
    @Execute
    public JsonResponse<List<ProductsResult>> get$index(ProductsListForm form) {
        validate(form, messages -> {});
        List<Product> productList = productsCrudAssist.selectProductList(form);
        List<ProductsResult> listResult = productsMappingAssist.mappingToListResult(productList);
        return asJson(listResult);
    }

    @Execute
    public JsonResponse<ProductsResult> get$index(Integer productId) {
        Product product = productsCrudAssist.selectProductById(productId);
        ProductsResult singleResult = productsMappingAssist.mappingToSingleResult(product);
        return asJson(singleResult);
    }

    @Execute
    public JsonResponse<Void> post$index(ProductsPostBody body) {
        validate(body, messages -> {});
        ...
        return JsonResponse.asEmptyBody();
    }

    @Execute
    public JsonResponse<Void> put$index(Integer productId, ProductsPutBody body) {
        validate(body, messages -> {});
        ...
        return JsonResponse.asEmptyBody();
    }

    @Execute
    public JsonResponse<Void> delete$index(Integer productId) {
        ...
        return JsonResponse.asEmptyBody();
    }

    ...
}

ネストリソースのExample e.g. /products/1/purchases/

例えば、/products/1/purchases/ というリソースがふたつの場合のActionの実装Exampleです。

e.g. action for nest resource /products/1/purchases/ @Java
@RestfulAction
public class ProductsPurchasesAction extends ShowbaseBaseAction {

    // ===================================================================================
    //                                                                           Attribute
    //                                                                           =========
    @Resource
    private ProductsPurchasesCrudAssist productsPurchasesCrudAssist;
    @Resource
    private ProductsPurchasesMappingAssist productsPurchasesMappingAssist;

    // ===================================================================================
    //                                                                             Execute
    //                                                                             =======
    @Execute
    public JsonResponse<List<ProductsPurchasesResult>>
            get$index(Integer productId, ProductsPurchasesSearchForm form) {
    ...
    @Execute
    public JsonResponse<ProductsPurchasesResult>
            get$index(Integer productId, Long purchasesId) {
    ...
  • メソッド名の前の改行は、単にドキュメントのデザインの都合上、横長だと見づらいので入れてるだけ

ハイフンつなぎリソース名

通常のLastaFluteのActionではハイフンつなぎの単語をサポートしていませんが、 RESTful APIではURLの形式に縛りがあるため、ハイフンを使いたくなる場面が増えると想定し、サポートしています。

hyphenate属性でハイフンつなぎを設定

例えば、/ballet-dancers/1/studios/2/ のとき、@RestfulActionのhyphenate属性にてハイフンでつないだときの表現である ballet-dancers を設定します。Actionのクラス名では、ハイフン区切りをキャメルケースにします。

e.g. action for hyphenated resource /ballet-dancers/1/studios/2/ @Java
@RestfulAction(hyphenate="ballet-dancers")
public class BalletDancersStudiosAction extends ShowbaseBaseAction {

パッケージは、普通のLastaFluteの規約通り、Actionのクラス名にそのまま相当する構成にします。

e.g. package for hyphenated resource /ballet-dancers/1/studios/2/ @Directory
app.web
 |-ballet
 |  |-dancers
 |  |  |-studios
 |  |  |  |-BalletDancersStudiosAction.java

ハイフンつなぎが複数ある場合

例えば、/ballet-dancers/1/favorite-studios/2/ のとき、hyphenate属性で二つ設定します。

e.g. action for hyphenated resource /ballet-dancers/1/favorite-studios/2/ @Java
@RestfulAction(hyphenate={"ballet-dancers", "favorite-studios"})
public class BalletDancersFavoriteStudiosAction extends ShowbaseBaseAction {

一文字のハイフンつなぎの場合

例えば、/a-dancers/1/b-studios/2/ のとき、Actionのクラス名で大文字が連続します。

e.g. action for hyphenated resource /a-dancers/1/b-studios/2/ @Java
@RestfulAction(hyphenate={"a-dancers", "b-studios"})
public class ADancersBStudiosAction extends ShowbaseBaseAction {

同じ組み合わせがもう一度登場する場合

例えば、/ballet-dancers/1/favorite-ballet-dancers/2/ のときでも、それぞれ設定します。

e.g. action for hyphenated resource /ballet-dancers/1/favorite-ballet-dancers/2/ @Java
@RestfulAction(hyphenate={"ballet-dancers", "favorite-ballet-dancers"})
public class BalletDancersFavoriteBalletDancersAction extends ShowbaseBaseAction {

hyphenate属性の検証

Action生成時に、LastaFlute内でhyphenate属性が厳しく検証されます。

  • hyphenate属性で利用できない文字 (e.g. 小文字半角英数ハイフン以外) があれば例外
  • hyphenate属性で指定したリソース名が、クラス名になかったら例外

EventSuffixでの回避

デフォルトでは[HTTP Method]$index()のみ

基本的に、ActionのExecuteメソッドは、必ず [HTTP Method]$index() で固定です。$より右はindex以外の名前を使うことはできません。

イベントごとのジレンマ

ただ、RESTfulの思想からは外れても(外れるのかどうか厳密にはわからないですが)、同じ HTTP Method の処理でも、どうしても業務で発生するイベントごとにメソッドを分けたくなった場合...例えば会員テーブルに対してイベントごとに検索条件も検索結果も大きく変わるような場合、get$index(Form) に集約すると巨大な最小公倍数のFormやJsonResultになってしまうかもしれません。 (パフォーマンスもそうですし、わかりやすさの面からメンテナンス性の低下が想定されます)

もしくは、以下のようにイベントごとに更新する項目が違うような場合、patch$index(Body) に集約すると巨大な最小公倍数のBodyになってしまうかもしれません。

  • 正式会員になったとき、正式会員日時に現在日時を入れて会員ステータスを正式会員にする
  • 会員情報編集の生年月日の入力で、生年月日に値が入る

put$index(body)で単に受け取ったままを更新するだけという風にしてイベントを意識しないやり方もありますが、それだとDBの業務ルールを呼び出しサービスに依存させてしまうことになるので、それが許容できなければなりません。 (DBのルールを散財させるとDB変更がしづらくなると想定されます)

EventSuffixでの回避

それぞれ、集約したままでも色々な回避策はあります。 検索に関しては、取捨選択するようなパラメーターを受け取るようにしたり、諦めたりなど。 更新に関しては、イベントを表現するパラメーターを受け取るようにしたり、そもそもそういうジレンマが発生しないようなDB設計をしたりなど。

ただもし、URLを分けてメソッドも分けることで解決するのであれば、@RestfulAction の allowEventSuffix を true にすることで、get$sea() や get$land() というようなメソッドを作ることができます。

get$index(form)
/products/ (そのまま)
get$sea(form)
/products/sea/
get$land(form)
/products/land/
get$index(Integer)
/products/1/ (そのまま)
get$sea(Integer)
/products/1/sea/
get$land(Integer)
/products/1/land/
e.g. action for nest resource /products/1/my-purchases/ @Java
@RestfulAction(allowEventSuffix=true)
public class ProductsAction extends ShowbaseBaseAction {

    @Execute
    public JsonResponse<List<ProductsResult>> get$sea(ProductsSeaSearchForm form) {
    ...

    @Execute
    public JsonResponse<List<ProductsResult>> get$land(ProductsLandSearchForm form) {
    ...

さきほどの、会員の更新の例だと...

patch$formalized(body)
/members/1/formalized/ (正式会員になったときの更新)
patch$birthdate(body)
/members/1/birthdate/ (生年月日情報の更新)

というように分けることになります。名前や粒度をどうするかはアプリでの慣習次第です。

EventSuffixが使えるかはrouter次第

ただし、この機能は RestfulRouter の実装に依存します。選択する Router によっては動作しません。 例えば、文字列のパスパラメーターを許容する Router だと、EventSuffixがパスパタメーターと区別が付かなくなり動作しません。 なので、パスパラメーターは必ず数値であることが前提になります。

RESTのCRUD枠にユースケースを当てはめてるのか?

そもそも、そのJSON APIは、CRUD要件のものなのか?ユースケース要件のものなのか?

例えば、"会員を登録する" というユースケースがあって、DB的には "会員" に加え "会員セキュリティ" と "会員サービス" などの関連テーブルinsertしないといけない場合、POST /members/ では何をするのか?

呼び出しサービスで、会員と会員セキュリティと会員サービスとリソースごとにリクエストを飛ばし、DBのトランザクションは諦めるか?自前でロールバックする仕組みを導入するか? などの工夫をするのであれば、その JSON API はCRUD要件のものと言え、RESTのCRUDにマッチしやすいので特に迷うことは少ないでしょう。 ただもちろん、その手前のレイヤにてユースケースやDBルールを解決するサービスが必要です。 (その呼び出しサービスの方で、ごちゃごちゃしないように注意を)

一方で、POST /members/ を会員登録とみなし、その中でそのユースケースに関するDBの処理をすべて行うのであれば、その JSON API はユースケース要件のものと言え、RESTのCRUD枠にユースケースに当てはめていると言えるでしょう。

その場合は、イベントごとのジレンマが発生しやすいと想定しています。いびつにならないように、気をつけて実装ポリシーを決めていきましょう。 jfluteとしては、引数リモコンパターンのようなAPIにならないことを望みます。

リソースはDBそのもの?DBインターフェース?

先ほど、正式会員になったときの更新、membersのPATCHではなく、formalizeds(造語)のPOSTとも言えるかもしれません。

リソースがDB構造が一致 (リソースはDBそのもの)
会員テーブルの更新だから、あくまで PATCH /members/1/(formalized/) で更新
でもDB変更で MEMBER_FORMALIZED というテーブルに切り出されたらAPIも変更する?
リソースはDB構造と似てるだけ (リソースはDBインターフェース)
内部的には会員テーブルの更新だけど、外向けには formalizeds というリソースがあるつもりで POST /members/1/formalizeds/ で登録(更新)
DB変更で切り出されてもAPIに影響なし。ただし、DBの構造に似てるとはいえ、リソース設計は独立してしっかりやる必要あり

この辺のニュアンスも、イベントごとのジレンマに影響しそうですね。

DBFluteとしては、あまりDB変更がしづらくなる構成はオススメではありません。

HTTP Statusの設定

成功系のHTTP Statusの設定方法

まず、デフォルトでは、成功系はすべて200です。

例えば、POSTの場合は201で、PUTやDELETEなどで戻りがない場合に204を戻したいなら...

規約ベース方式
ActionHookでExecuteメソッドの構造から自動で判別するように
アノテーション方式
Executeアノテーションで successHttpStatus属性を指定

規約ベース方式

規約ベース方式なら、TypicalStructuredSuccessHttpStatusHandlerというクラスが用意されています。これは ActionHook の hookFinally() で使うと良いでしょう。(もし、アプリ要件に合うのであれば)

e.g. TypicalStructuredSuccessHttpStatusHandler in hookFinally() @Java
@Override
public void hookFinally(ActionRuntime runtime) {
    super.hookFinally(runtime);

    // for RESTful HTTP status as conventional way e.g. POST:201, DELETE(no return):204
    new TypicalStructuredSuccessHttpStatusHandler().reflectHttpStatusIfNeeds(runtime);
}

組み込みのHandlerが合わなければ、自前で似たような実装をすると良いでしょう。

ただ、SwaggerUIの情報と同期をするためには、SwaggerActionのSwaggerGeneratorのoptionにて、そのHandlerを連携させてあげる必要があります。 (詳しくは、SwaggerOptionクラスのコードにて)

アノテーション方式

アノテーション方式なら、Executeアノテーションで successHttpStatus属性を指定します。ただし、こちらはつどつど指定することになるので、ケアレスミスは発生しやすくなります。

e.g. successHttpStatus on @Execute annotation @Java
@Execute(successHttpStatus = @HttpStatus(value = 201, desc = "new product is created"))
public JsonResponse<Void>s post$index(ProductsPostBody body) {

かなりゴツゴツしているのは、説明(desc)を必須にしているからです。明示的に指定するということは、何かしら説明が必要なケースであろうと想定されるためです。

という "ゴツゴツさ" から、すべてをアノテーション方式だけでやるというのはオススメしません。規約ベース方式の中で、特殊ケースのときにだけ使うというのが良いでしょう。

SwaggerUIの情報とは自動的に同期されます。

asJson方式???

asJson(...).httpStatus(201)のメソッドはオススメしません。 Executeメソッド内での設定なので動的な変化などには対応しやすいですが、レアな要件だと想定されますし、SwaggerUIの情報とは同期が取れませんので、RESTful API で HTTP Status にこだわっているのであれば、使わないほうが良いと思われます。

例外系のHTTP Statusの設定方法

まず、LastaFluteのデフォルトでは、業務例外とクライアント例外はすべて400 (一部404) となり、システム例外はすべて500ですが、ApiFailureHookの実装次第で、そのロジックを自由に調整できます。

例えば、例外時レスポンスのJSONポリシーが "Failure統一クライアントメッセージ" で TypicalFaicliApiFailureHook を利用している場合は、setupBusinessHttpStatusMap()をオーバーライドすることで、特定の例外クラスに対して特定のHTTPステータスを関連付けることができます。

e.g. override setupBusinessHttpStatusMap of TypicalFaicliApiFailureHook @Java
@Override
protected void setupBusinessHttpStatusMap(Map<Class<?>, Integer> failureMap) {
    // you can add mapping of failure status with exception here
    failureMap.put(AccessTokenUnauthorizedException.class, HttpServletResponse.SC_UNAUTHORIZED);
    failureMap.put(AccessUnderstoodButRefusedException.class, HttpServletResponse.SC_FORBIDDEN);
}

RestfulRouterの設定

ActionAdjustmentProviderにてrouterを

LastaFluteは、デフォルトではRestfulActionに対応していません。

ActionAdjustmentProvider の customizeActionUrlMapping() と customizeActionUrlReverse() で RestfulRouterを設定する必要があります。

Exampleプロジェクトである lastaflute-example-maihamaサービスの maihama-showbaseアプリにてExampleがありますので、そちらを参考に設定しましょう。

どのRestfulRouterを使う?

RestfulRouterは、実装方法に選択肢があるため、アプリの要件にあったものを選びます。

NumericBasedRestfulRouter
IDが必ず数値であることを前提
PairBasedRestfulRouter
リソース名とIDがペアで構成されることを前提

例えば、NumericBasedRestfulRouterの方は、IDが必ず数値である必要がありますが、EventSuffixを使うことができます。 一方で、PairBasedRestfulRouterは、EventSuffixは使えませんが、文字列のIDを利用することができます。

その他細かい挙動の違いがあるので、必要に応じてコードを読みましょう。 (2021/06/06時点では、NumericBasedRestfulRouterの方に力を入れています)