emahiro/b.log

日々の勉強の記録とか育児の記録とか。

配列形式のクエリパラメータを受け取る実装について考えてみる

Overview

ID=1, ID=2,... のようにクエリパラメータで指定したパラメータを key にしてリソースを取得する GET の API、というのは一般的かと思いますが、このクエリパラメータで同じ key を複数取って複数のリソースを取得する実装パターンについて API を実装するときのベターなプラクティスとそれに伴う Go の実装について考えてみます。

Motivation

API を書いていたときに RDB 上の PrimaryKey をクエリパラメータに指定してあるリソースを複数取得する Endpoint を実装するときに、クエリパラメータの取り方で実装のフィードバックを受けたのがきっかけです。

実装パターンについて

以下のパターンをベースに考えます。

  1. GET /user?id=1,2,3 という 取得したいパラメータのリストをカンマ区切りの文字列 で表現する方法。
  2. GET /user?id=1&id=2&id=3 という 取得したいパラメータを1つずつ & で繋げて配列形式を表現する 方法。
  3. GET /user?id[]=1&id[]=2&... という 2 と似ているが取得したいパラメータが「配列である」ことを表現するために [] のブラケットをつける 方法。

GET /user?id=1,2,3 のパターン

ほぼ癖ですが、僕は自分が Overview に記載されているような GET の Endpoint を実装するときはほとんどこのパターンを採用してました。強い理由はないですが、URL からもあるリソースに対してどういうフィルタが適用されているのかが直感的でわかりやすいと考えていたからです。

Go で実装するときも以下のようにクエリパラメータを取り出して、split 分割で配列に変換して後続の DB レイヤの処理に回す、という実装をしていました。

var ids []int
query := r.URL.Query()
if q := query.Get("ids"); q != "" {
    split := strings.Split(q, ",")
    if len(split) != 0 {
        for _ , idstr := range split {
            id, _ := strconv.Atoi(idstr)
            ids = appned(ids, id)
        }
    }
}

ただ、このようなクエリパラメータのパターンは iOS で一般的に使用されている Alamofire のような HTTP クライアントのライブラリではそのままで対応してないです ※ ※ 配列を表現したいときは ids[] というようなブラケットが付いてしまいます。

dev.classmethod.jp

Go の実装面でもカンマ区切りの部分を Split して文字列を分割する必要があり、ちょいと工夫が必要です。

GET /user?id=1&id=2&id=3&id=... のパターン

URL 上は若干読みづらい(直感的ではない)ですが、配列にしたい key を複数クエリパラメータに指定することで「配列である」ことを表現します。

この場合、Go 側の実装では以下のようになります。

var ids []int
query := r.URL.Query()
if idsStr = query["id"]; len(idsStr) > 0 {
    for _ , idStr := range idsStr {
        id, _ := strconv.Atoi(idStr)
        ids = append(ids, id)
    }
}

これだと Go 側では文字列の分割処理をする必要がないです。

パターンとしても直感的ではないものの、 Endpoint のクエリパラメータで配列を表現するには一般的らしいです。

stackoverflow.com

GET /user?id[]=1&id[]=2&id[]=3&... のパターン

2 のパターンと同じですが、先に紹介した iOS のライブラリのようにクエリパラメータの中で、クエリパラメータが配列であることを表現します。

Go 側の実装は 2 のときとあまり変わりません。

var ids []int
query := r.URL.Query()
if idsStr = query["id[]"]; len(idsStr) > 0 {
    for _ , idStr := range idsStr {
        id, _ := strconv.Atoi(idStr)
        ids = append(ids, id)
    }
}

RFC 3986 を見る

もともと カンマ区切りの文字列 を採用してるときに iOS との連携時に、クエリパラメータがうまくデコードできないということに気づいて今回のことを考えました。

iOS としては元々 3 のパターンを前提としてクエリパラメータをデコードしていたのですが、どうやら iOS 側で利用してるメジャーなライブラリ(現職の iOS チームが使っていたもの)が RFC 3986 に対応したことで 3 のパターンが使えなくなり、結果としては 2 のパターンで実装する事になりました。

なお、この RFC3986 に対応したことでデコードエラーになるようになったのは、 []予約語扱いになったので、ライブラリ側でデコードエラーとするようになったっぽいです。
ref: https://tex2e.github.io/rfc-translater/html/rfc3986.html#2-2--Reserved-Characters

まとめ

クエリパラメータの中での配列の表現について自分の実装の癖を見直すいいきっかけになりました。
動作としては同じでもモバイル側の都合等々、考慮しないといけないポイントがありますね。