きっかけ
それは、Springの諸々をKotlin書いていたときの出来事です。
id指定で1件レコードを返すAPIを愚直に書いていたのですが…
(関数名の適当さは目をつぶってほしい)
Controller
@GetMapping("{id}") fun getAddress(@PathVariable("id") id: Int): FindAddressResponse { return addressService.find(id) }
Service
fun find(addressId: Int): FindAddressResponse { val result = addressDomaDao.find(addressId) return FindAddressConverter.of(result) }
Dao
@Select AddressReadEntity find(int addressId);
Converter
fun of(from: AddressReadEntity): FindAddressResponse { return FindAddressResponse( personId = from.personId, addressId = from.addressId, name = from.name, sexType = from.sexType, address = from.address ) }
ここでふと… 存在しないidが引数で指定されたときにDaoからnullが返るからServiceあたりで落ちるのでは…?
実行結果
java.lang.IllegalStateException: result must not be null at jp.takeda.doma2sample.domain.service.AddressService.find(AddressService.kt:25) ~[main/:na] at jp.takeda.doma2sample.controller.AddressController.getAddress(AddressController.kt:22) ~[main/:na]
うん、落ちたね。
こういう場合にAPI的には何を返したらいいのか。
また、Kotlin的なカッコヨイ処理の仕方って何だろう。
この時点のコード。
GitHub - takedasan/doma2samplekotlin at afeb44af77492b9c3fcb4817d4cdd0757dc495f5
何を返したらいいのか
これに同意。
HTTP GET REST API — No Content — 404 vs 204 vs 200 – Santhosh Kumar Krishna – Medium
(3/10) twitterでコメント頂いてちょっと考えなおしました。『HTTPステータスは何を返すべきなのか(再)』に追記します
ステータスコードは200、Bodyは空。
ほかの取得結果と同じで、結果なかったよーという正常レスポンスを返すのが素直な気がする。
カッコヨイ処理の仕方
いくつかありそう。
Javaおじさんとtry-catch構文
まず思いつくのは、ServiceなりDaoなりで任意の例外を投げて、Controllerでcatch。
その場合に空のレスポンスを返すというもの。
Controller
@GetMapping("{id}") fun getAddress(@PathVariable("id") id: Int): ResponseEntity<FindAddressResponse> { return try { ResponseEntity.ok(addressService.find(id)) } catch (e: IllegalArgumentException) { ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).build<FindAddressResponse>() } }
Service
fun find(addressId: Int): FindAddressResponse { val result = addressDomaDao.find(addressId) result ?: throw IllegalArgumentException("存在しないidですよ") return FindAddressConverter.of(result) }
なんかこの感じ落ち着く…
実家のような安心感。
これでも機能としては十分満たしていると思うのですよね。
この時点でのコード
GitHub - takedasan/doma2samplekotlin at 7d613f97f116593224e6cf90425fce297e359549
🤔なところ
Serviceで0件の時に例外投げるのでControllerでキャッチしてね!みたいなルールができちゃうところ。
(あと、今bodyって空なので"{}"って返したい場合ってどう書けばいいんですかね…)
(3/10) twitterでコメント頂きました!『body部に"{}"でレスポンスを返したい』に追記します
あと単純に結果がなかったら空にしたいだけなのにtry-catchが出てくるのが違和感。
レスポンス的には異常(404)じゃないぜ正常(200)だぜーヘイヘイ!って感じなのに、内部の処理はこんなん。
別に結果がないのは例外じゃなくない…?
whenとかifで振り分けたほうが素直な感じしないですか。
シールドクラスによるエラーの表現
ここの記事読んで、感動してしまったのですが、こんな書き方あるんですねぇ…
例外だけに頼らない Kotlin のエラーハンドリング - Qiita
ControllerとServiceを書き換えればよさそう
Controller
@GetMapping("{id}", produces = ["application/json"]) fun getAddress(@PathVariable("id") id: Int): ResponseEntity<FindAddressResponse> { val result = addressService.find(id) return when (result) { is FindAddressResult.Found -> ResponseEntity.ok(result.response) is FindAddressResult.NotFound -> ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).build<FindAddressResponse>() } }
Service
fun find(addressId: Int): FindAddressResult { val result = addressDomaDao.find(addressId) // nullだった場合は結果なしとして扱う return if (result == null) { FindAddressResult.NotFound } else { FindAddressResult.Found(FindAddressConverter.of(result)) } }
やったぜハッピー。例外を追い出せた。
これはKotlinっぽくてカッコイイ。
この時点でのコード
GitHub - takedasan/doma2samplekotlin at 404af4598e2f42dd6cc19875fb1af4f34dbc2c17
(3/10追記)body部に"{}"でレスポンスを返したい
公開した後に、このお悩みをボソッとつぶやいたのですが、
リプライで方法を教えてもらったので追記です。
(感謝 🙏。ブログ書いててよかった!)
Springでbody部を"{}"で返す方法いまだに謎。
— takedasan (@takedasan593) March 9, 2019
空のクラスをつくる
kotlin はわからないですが、空の JSON クラスを作ってしまうのはどうですか?https://t.co/f6UN7GQF0K
— とばち (@toda_kk) March 9, 2019
空のクラスですって!?
試してみよう。
Empty response
@JsonSerialize class EmptyResponse
Controller
@GetMapping("{id}") fun getAddress(@PathVariable("id") id: Int): ResponseEntity<Any> { val result = addressService.find(id) return when (result) { is FindAddressResult.Found -> ResponseEntity.ok(result.response) is FindAddressResult.NotFound -> ResponseEntity.ok(EmptyResponse()) } }
動作確認
curl http://localhost:8080/addresses/4 {}
直感的で分かりやすい!
(EmptyResponseみたいな名前にすれば空を返しているのがよくわかる)
あとは戻り値のAnyを消し去るところが腕の見せ所さん。
Collections.emptyMap()を使う
Collections.emptyMap()でそうなりそうな気がします。
— うらがみ⛄ (@backpaper0) March 9, 2019
空のMapですって!?
試してみよう。
Controller
@GetMapping("{id}") fun getAddress(@PathVariable("id") id: Int): ResponseEntity<Any> { val result = addressService.find(id) return when (result) { is FindAddressResult.Found -> ResponseEntity.ok(result.response) is FindAddressResult.NotFound -> ResponseEntity.ok(Collections.EMPTY_MAP) } }
※型の関係でEMPTY_MAPにしました
動作確認
curl http://localhost:8080/addresses/4 {}
Controller内だけで解決できるのがうれしい。
これも戻り値のAnyを消し去るところが腕の見せ所さん。
結局どうするのがよいのか(個人的に)
クライアント側が、body部ないと受け取れないんですけど💢
みたいなことでなければ、なしでもよいと思います。
ここのAny型を消し去るために複雑なことをやるぐらいなら、やらんでもなぁという気持ち。
参照した記事にも書いてある通り、やんごとなき理由で"{}"を返す場合はこれらの方法で返してあげるのがよさそう。
(それが受け取るクライアント側にとって優しいかは、いったん置いておく…)
(3/10追記)HTTPステータスは何を返すべきなのか(再)
完璧なコードやんと思ってスッターン!とpushして、
一晩寝かしたコードを見てみると意外に直すところがみつかることがありますね。
今そんな気持ちです。
結果なしなので、HTTP ステータスは 404 NotFound のほうが適切な気がします。結果なしの場合、私はサービスにて Kotlin の Null 許容型 (今回だと FindAddressResponse? 型) を返すようにしています。参考までに。
— DJらびたろ (@rabitarochan) March 9, 2019
URLに対応するリソースがなければ404を返却する。
確かに静的なリソースにアクセスした場合と合わせて考えると、レスポンスの期待ってそっちな気がします。
今回の場合、クライアント側が、200台じゃないとないと例外としてcatchしちゃうんですけど💢
というやんごとなき理由もあるので、200で返すことにしていたのだけれども、素直なのは404だなぁという気持ち。
(驚くべきことにクライアントとサーバのアプリ両方、自分で設計してるのに…というのがアレな感じ)