投稿日:
2020年2月25日
最終更新日:
【bindFromRequestData】playFramework2.7で配列をマッピングする方法【BeanValidator】
YouTubeも見てね♪
ねこじゃすり
猫を魅了する魔法の装備品!
Anker PowerCor
旅行には必須の大容量モバイルバッテリー!
[ノースフェイス] THE NORTH FACE メンズ アウター マウンテンライトジャケット
防水暴風で耐久性抜群なので旅行で大活躍です!
ドラゴンクエスト メタリックモンスターズギャラリー メタルキング
みんな大好き経験値の塊をデスクに常備しておこう!
Bauhutte ( バウヒュッテ ) 昇降式 L字デスク ブラック BHD-670H-BK
メインデスクの横に置くのにぴったりなおしゃれな可動式ラック!
BANDAI SPIRITS ULTIMAGEAR 遊戯王 千年パズル 1/1スケール
もう一人の僕を呼び覚ませ!!
MOFT X 【新型 ミニマム版】 iPhone対応 スマホスタンド
Amazon一番人気のスマホスタンド!カード類も収納出来てかさ張らないのでオススメです!
目次
配列クエリを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
にアクセスして確認してみましょう。
これで一応期待通りの動きになりましたね!
終わりに
痒いところに手が届かない部分を自前でカスタマイズしてみました。
他にも良い対応策があると思いますが、同じようにモヤモヤしている方は参考にしてみていただければなと思います。