Anker PowerCor
旅行には必須の大容量モバイルバッテリー!
ペヤング ソースやきそば 120g×18個
とりあえず保存食として買っておけば間違いなし!
レッドブル エナジードリンク 250ml×24本
翼を授けよう!
モンスターエナジー 355ml×24本 [エナジードリンク]
脳を活性化させるにはこれ!
BANDAI SPIRITS ULTIMAGEAR 遊戯王 千年パズル 1/1スケール
もう一人の僕を呼び覚ませ!!
MOFT X 【新型 ミニマム版】 iPhone対応 スマホスタンド
Amazon一番人気のスマホスタンド!カード類も収納出来てかさ張らないのでオススメです!
サンディスク microSD 128GB
スマホからSwitchまで使える大容量MicroSDカード!
スポンサーリンク
目次
配列クエリをFormモデルにバインドしようとしたら一癖あった話
PlayFrameworkを使ってAPI開発をしている際に、配列を想定しているクエリパラメーターに対してもBeanバリデーションを行いたかったため、Formモデルを使ってマッピングしようとしたところ、最初の要素しかListモデルにマッピング出来ませんでした。
今回はその原因と実際に試してみた対処方法についてご紹介しようと思います。
手順
前提
以下のようなidを複数指定して返却するようなAPIがあったとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
openapi: 3.0.0 info: title: サンプル version: 1.0.0 paths: /v1/accounts: get: parameters: - name: ids in: query required: false schema: type: array items: type: integer format: int32 responses: 200: description: 成功時のレスポンス content: 'application/json': schema: type: object properties: accounts: type: array items: type: object properties: id: type: integer format: int32 example: 1001 name: type: string example: 山田太郎 age: type: integer format: int32 example: 26 total: type: integer format: int32 example: 1 |
まずはModelにマッピングせずに直接取得する方法
まずはController側で直接配列のクエリパラメーターを受け取ってみましょう。
以下のようなroutes
の記述とController
があったとします。
1 2 3 4 5 6 7 8 |
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ GET /v1/accounts controllers.account.AccountController.listingBy(ids: java.util.List[Integer]) ... (略) ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package controllers.account; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; import models.account.AccountListElementModel; import models.account.AccountListModel; import play.mvc.Controller; import play.mvc.Result; public class AccountController extends Controller { public Result listingBy(List<Integer> ids) { ... (略) ... |
確認
この場合ids
の値は、http://localhost:9000/v1/accounts?ids=1&ids=4&ids=6
の場合はids
は1
と4
と6
、http://localhost:9000/v1/accounts?ids=1
の場合は1
、http://localhost:9000/v1/accounts
の場合はids
はempty(nullではない)となります。
しっかりマッピング出来てますね♪
Formモデルで取得
しかし、routes
とController
の引数に直接追加すると、クエリパラメーターが増えたりした際の修正範囲が増えてしまいます。
PlayFrameworkのFormはPOST
などのリクエストボディのマッピングで主に使うようですがGET
のクエリパラメーターに対してもモデルにマッピングし、さらにBeanバリデーションを活用したいので試してみましょう。
以下の検索モデルを用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package models.account; import java.util.List; import javax.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @AllArgsConstructor @Builder @Data public class AccountListCriteriaModel { @Size(max = 3) public List<Integer> ids; public AccountListCriteriaModel(){} // LombokのNoArgsConstructorが認識されないので明示的に定義 } |
今回はとりあえずなにかしらのBeanバリデーションをかけたいので、@Size
を利用し要素が4件以上指定された場合にバリデーションエラーを発生させるようにしてみます。
routes
とController
は以下のように修正します。
1 2 3 4 5 6 7 8 9 |
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ GET /v1/accounts controllers.account.AccountController.listingBy(request: Request) ... (略) ... |
確認
では、実際にリクエストしてみましょう。
まず、http://localhost:9000/v1/accounts
の場合はids=null
となります。この場合はControllerの引数で直接取得する場合との差異が出てますね。
次にhttp://localhost:9000/v1/accounts?ids=1
の場合。
こちらは正しくListの要素として4要素がマッピングされていますね。
最後に複数クエリパラメーターが指定されたhttp://localhost:9000/v1/accounts?ids=1&ids=4&ids=8&ids=10
の場合です。
あら、一つ目の要素しかマッピングされていませんし、Beanバリデーターも機能していませんね。。。
試しにCriteriaモデルのids
の型をListから配列にしてもダメでした。orz
配列をマッピングしたい場合は[]を付けるのがplayのお作法
実は、PlayFrameworkでは配列パラメーターをマッピングしたい場合は[]
を付けるのが基本となっているようです。
試しに、http://localhost:9000/v1/accounts?ids[]=1&ids[]=4&ids[]=8&ids[]=10
でリクエストしてみましょう。
正しくids
に4要素マッピングされ、Beanバリデーションも実行されていますね!
とはいえ、[]を付けるのはちょっと気持ち悪い。。。
しかし、クエリパラメーターに[]
を付けるのはちょっと一般的ではない気がするのと、呼び出し側も修正しないといけないのでちょっと採用したくない案ですよね。。。
そこで、クエリには[]
をつけずにFormでバインドしてBeanバリデーションを実行する方法を以下のようにして実現してみました!(もっとスマートな方法があるかもしれません。orz)
配列を[]を使わずにFormモデルにbind&Beanバリデーションを実行する方法
routesの修正
まず、routes
の設定として配列になりうる要素のみController
の引数で取得出来るようにします。
1 2 3 4 5 6 7 8 9 |
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ GET /v1/accounts controllers.account.AccountController.listingBy(request: Request,ids: java.util.List[String]) ... (略) ... |
この際に、String
で定義しておくことがポイントです。
ControllerでFormにbindする部分を修正
次に、Controller
の引数で受け取った配列を利用して意図的にバインドする処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
package controllers.account; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; import javax.inject.Inject; import javax.inject.Singleton; import models.account.AccountListCriteriaModel; import models.account.AccountListElementModel; import models.account.AccountListModel; import play.data.Form; import play.data.FormFactory; import play.i18n.MessagesApi; import play.mvc.Controller; import play.mvc.Http; import play.mvc.Result; @Singleton public class AccountController extends Controller { private final MessagesApi messagesApi; private final Form<AccountListCriteriaModel> accountListCriteria; private final String CODES_QUERY_KEY = "ids"; // 配列クエリのキーを定義 @Inject public AccountController(FormFactory formFactory, MessagesApi messagesApi) { this.messagesApi = messagesApi; this.accountListCriteria = formFactory.form(AccountListCriteriaModel.class); } public Result listingBy(Http.Request request, List<String> ids) { // クエリに含まれている配列があり得るキーの値を一旦除去 Map<String, String[]> ignoreArrayQueryStringMap = request.queryString().entrySet().stream() .filter(entry -> !Arrays.asList(CODES_QUERY_KEY).contains(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // Listを配列に変換 String[] convertCodes = new String[ids.size()]; ids.toArray(convertCodes); // 受け取った配列をMapに追加 ignoreArrayQueryStringMap.put(String.format("%s%s", CODES_QUERY_KEY, "[]"), convertCodes); // 処理を加えたMapを使ってbind final Form<AccountListCriteriaModel> bindForm = accountListCriteria.bindFromRequestData( this.messagesApi.preferred(request).lang(), request.attrs(), ignoreArrayQueryStringMap); ... (略) ... |
確認
では、http://localhost:9000/v1/accounts?ids=1&ids=4&ids=8&ids=10
にアクセスして確認してみましょう。
これで一応期待通りの動きになりましたね!
終わりに
痒いところに手が届かない部分を自前でカスタマイズしてみました。
他にも良い対応策があると思いますが、同じようにモヤモヤしている方は参考にしてみていただければなと思います。