query.Values
クエリパラメータを明示的に処理する。
概要
query.Values は、HTTP クエリーパラメータ全体の読み取り専用ビューを提供します。 SpineはクエリパラメータをDTOに自動的にマッピングしません。代わりに、query.Valuesを介してControllerが直接必要な値を明示的に取り出すように設計されています。
##なぜ自動マッピングではないのですか?
ほとんどのフレームワークは、クエリパラメータをstructタグに自動バインドします。
// 他のフレームワークの方式
type SearchParams struct {
Status string `query:"status"`
Tags []string `query:"tag"`
Page int `query:"page"`
}
func Search(params SearchParams) { ... }Spineはこのアプローチを採用していません。
理由 1: 明示性
クエリパラメータは可変でオプションです。自動マッピングは「どのパラメータがどこから来たのか」を隠します。
// Spine 方式: 明示的 抽出
func Search(q query.Values) []User {
status := q.String("status") // 明確な出所
page := q.Int("page", 1) // デフォルト値を明示
if q.Has("premium") { // 条件付き処理
// ...
}
}理由 2: 柔軟性
検索APIのように動的なクエリを扱う場合、structベースのバインディングは制約になります。
// 動的フィルタ処理
func Search(q query.Values) []Product {
filters := make(map[string]string)
// どのような フィルターが 来るか あらかじめ 分からない
if q.Has("min_price") {
filters["min_price"] = q.String("min_price")
}
if q.Has("max_price") {
filters["max_price"] = q.String("max_price")
}
if q.Has("category") {
filters["category"] = q.String("category")
}
return c.repo.FindByFilters(filters)
}Values 構造体
// pkg/query/types.go
type Values struct {
values map[string][]string
}
func NewValues(values map[string][]string) Values {
return Values{values: values}
}Valuesはmap[string][]stringを包むラッパーです。各キーは複数の値を持つことができます(例:?tag=go&tag=web)。
メソッド
Get(key string) string
指定したキーの最初の値を文字列として返します。キーがない場合は空の文字列を返します。
// GET /users?name=john&status=active
q.Get("name") // "john"
q.Get("status") // "active"
q.Get("missing") // ""String(key string) string
Get()と同じです。エイリアスとして提供されます。
q.String("name") // "john"Int(key string, def int64) int64
指定したキーの値を整数として解析します。解析失敗またはキーがない場合はデフォルト値を返します。
// GET /users?page=3&size=20
q.Int("page", 1) // 3
q.Int("size", 10) // 20
q.Int("offset", 0) // 0 (キーなし → デフォルト値)
q.Int("page", 1) // 1 (もし page=abcなら → デフォルト値)GetBoolByKey(key string, def bool) bool
指定したキーの値をブーリアンとして解析します。値を小文字に変換して判別します。
trueとして認識:"true"、"1"、"yes"、"y"、"on"(大文字と小文字を無視)
falseとして認識:"false"、"0"、"no"、"n"、"off"(大文字と小文字を無視)
// GET /users?active=true&verified=1&premium=yes
q.GetBoolByKey("active", false) // true
q.GetBoolByKey("verified", false) // true
q.GetBoolByKey("premium", false) // true
q.GetBoolByKey("deleted", false) // false (キーなし → デフォルト値)
q.GetBoolByKey("active", false) // false (もし active=maybe → デフォルト値)Has(key string) bool
指定したキーが存在することを確認してください。値が空でもキーがある場合はtrueです。
// GET /users?status=active&empty=
q.Has("status") // true
q.Has("empty") // true (値は 空ですだけ キーは 存在)
q.Has("missing") // falseQueryValuesResolver
query.ValuesタイプをControllerパラメータとして使用すると、QueryValuesResolverは自動的に値を生成します。
// internal/resolver/query_values_resolver.go
type QueryValuesResolver struct{}
func (r *QueryValuesResolver) Supports(pm ParameterMeta) bool {
return pm.Type == reflect.TypeFor[query.Values]()
}
func (r *QueryValuesResolver) Resolve(ctx core.ExecutionContext, parameterMeta ParameterMeta) (any, error) {
httpCtx, ok := ctx.(core.HttpRequestContext)
if !ok {
return nil, fmt.Errorf("HTTP リクエスト コンテキストが ではありません")
}
return query.NewValues(httpCtx.Queries()), nil
}動作原理
- PipelineがControllerシグネチャを分析する
query.Valuesタイプパラメータ発見QueryValuesResolver.Supports()→trueQueryValuesResolver.Resolve()呼び出しExecutionContextをHttpRequestContextにタイプ断言httpCtx.Queries()でフルクエリマップを取得query.NewValues()でラップして返す
注: Resolverは
core.ExecutionContextを受け取った後、core.HttpRequestContextにタイプ断言します。 HTTP要求ではなくコンテキスト(Consumer、WebSocket)はエラーを返します。
使用例
デフォルトの使用
// cmd/demo/controller.go
func (c *UserController) GetUserQuery(q query.Values) User {
return User{
ID: q.Int("id", 0),
Name: q.String("name"),
}
}# リクエスト
GET /users?id=123&name=john
# レスポンス
{
"id": 123,
"name": "john"
}検索API
func (c *ProductController) Search(q query.Values) SearchResult {
keyword := q.String("q")
category := q.String("category")
minPrice := q.Int("min_price", 0)
maxPrice := q.Int("max_price", 999999)
inStock := q.GetBoolByKey("in_stock", true)
products := c.repo.Search(SearchCriteria{
Keyword: keyword,
Category: category,
MinPrice: minPrice,
MaxPrice: maxPrice,
InStock: inStock,
})
return SearchResult{
Query: keyword,
Count: len(products),
Products: products,
}
}GET /products?q=laptop&category=electronics&min_price=500&in_stock=trueページネーションで使用
query.Valuesとquery.Paginationを併用できます。
func (c *UserController) List(p query.Pagination, q query.Values) PagedResult {
status := q.String("status")
sortBy := q.String("sort_by")
users := c.repo.FindAll(status, sortBy, p.Page, p.Size)
total := c.repo.Count(status)
return PagedResult{
Data: users,
Page: p.Page,
Size: p.Size,
Total: total,
}
}GET /users?page=2&size=20&status=active&sort_by=created_at条件付きフィルタ
func (c *OrderController) List(q query.Values) []Order {
filters := OrderFilters{}
if q.Has("user_id") {
filters.UserID = q.Int("user_id", 0)
}
if q.Has("status") {
filters.Status = q.String("status")
}
if q.Has("from_date") {
filters.FromDate = parseDate(q.String("from_date"))
}
if q.Has("to_date") {
filters.ToDate = parseDate(q.String("to_date"))
}
return c.repo.FindByFilters(filters)
}多値処理
クエリパラメータは、同じキーで複数の値を渡すことができます。
GET /products?tag=go&tag=web&tag=api現在、query.ValuesのString()、Get()メソッドは最初の値のみを返します。複数の値が必要な場合は、内部マップに直接アクセスするメソッドを追加したり、コンマ区切り値を解析する方法を使用できます。
// カンマ 区切り 方式
// GET /products?tags=go,web,api
func (c *ProductController) Search(q query.Values) []Product {
tagsRaw := q.String("tags")
tags := strings.Split(tagsRaw, ",")
return c.repo.FindByTags(tags)
}query.Paginationとの違い
|特性query.Values | query.Pagination | |------|--------------|------------------| | 用途 |可変クエリパラメータ|固定ページネーション | パラメータ |すべてのクエリpage、sizeのみ| | デフォルト |メソッド呼び出し時の指定自動適用(page = 1、size = 20)| | タイプ変換 |明示的自動
使用選択基準
// 固定の ページネーションだけ 必要 → query.Pagination
func List(p query.Pagination) []User
// 動的 フィルター + ページネーション → 両方 使用
func Search(p query.Pagination, q query.Values) []User
// 完全に 動的のクエリ → query.Valuesだけ
func CustomSearch(q query.Values) []User設計原則
1. 明示的な抽出
// ✓ Spine: どこから来たか 明確
status := q.String("status")
page := q.Int("page", 1)
// ❌ 自動 バインディング: 出所 不明確
func Search(params SearchParams) // statusが query? body? path?2. デフォルト値の指定
// ✓ デフォルト値このコードに 明示
page := q.Int("page", 1)
size := q.Int("size", 20)
// ❌ struct タグの デフォルト値は 非表示
type Params struct {
Page int `query:"page" default:"1"` // どこから 設定なったかどうか 追跡困難
}3. オプションのパラメータ処理
// ✓ Has()に 存在 有無 明示的 確認
if q.Has("premium") {
filters.Premium = q.GetBoolByKey("premium", false)
}
// ❌ 自動 バインディングは zero valueと "値 なし"を 区別できません
type Params struct {
Premium bool `query:"premium"` // falseが デフォルト値かどうか 明示的 falseかどうか?
}まとめ
|メソッド戻りタイプ用途| |--------|----------|------| | Get(key) | string |文字列値(存在しない場合は"")| | String(key) | string | Get()のエイリアス| | Int(key, def) | int64 |整数値(失敗時のデフォルト)| | GetBoolByKey(key, def) | bool |ブール値(失敗時のデフォルト)| | Has(key) | bool |キーが存在するかどうか
核心哲学:Spineはクエリパラメータを「魔法のように」自動マッピングしません。 query.Values を介して Controller が必要な値を明示的に取り出し、書き込みます。これはSpineの「No Magic」原則と一致しています。
