最近几天我一直在为如何在Go REST API中处理PATCH请求而苦恼,直到我找到了一篇关于使用指针和omitempty
标签的文章,我按照其中的方法实现后发现工作正常。但是后来我意识到我仍然需要构建一个UPDATE
SQL查询。
我的struct
看起来像这样:
type Resource struct {
Name *string `json:"name,omitempty" sql:"resource_id"`
Description *string `json:"description,omitempty" sql:"description"`
}
我期待一个包含以下请求体的
PATCH /resources/{resource-id}
请求:{"description":"Some new description"}
在我的处理程序中,我将按照以下方式构建
Resource
对象(忽略导入和错误处理):var resource Resource
resourceID, _ := mux.Vars(r)["resource-id"]
d := json.NewDecoder(r.Body)
d.Decode(&resource)
// at this point our resource object should only contain
// the Description field with the value from JSON in request body
现在,对于普通的
UPDATE
(PUT
请求),我会这样做(简化版):stmt, _ := db.Prepare(`UPDATE resources SET description = ?, name = ? WHERE resource_id = ?`)
res, _ := stmt.Exec(resource.Description, resource.Name, resourceID)
使用
PATCH
和omitempty
标签的问题在于对象可能缺少多个属性,因此我不能只准备一个带有硬编码字段和占位符的语句......我必须动态构建它。那么问题来了:我该如何动态构建这样的
UPDATE
查询?在最好的情况下,我需要一些解决方案来识别设置属性,获取它们的SQL字段名称(可能是从标记中获取),然后我应该能够构建UPDATE
查询。我知道我可以使用反射来获取对象属性,但不知道如何获取它们的sql标记名称,当然,如果可能的话,我想避免在这里使用反射......或者我可以简单地检查每个属性是否不是nil
,但在现实生活中,结构体比提供的示例要大得多...有人能帮我解决这个问题吗?有人已经不得不解决相同/类似的情况吗? 解决方案:
根据这里的答案,我能够想出这个抽象的解决方案。 SQLPatches
方法从给定的结构体中构建 SQLPatch
结构体(因此没有具体的结构体特定):
import (
"fmt"
"encoding/json"
"reflect"
"strings"
)
const tagname = "sql"
type SQLPatch struct {
Fields []string
Args []interface{}
}
func SQLPatches(resource interface{}) SQLPatch {
var sqlPatch SQLPatch
rType := reflect.TypeOf(resource)
rVal := reflect.ValueOf(resource)
n := rType.NumField()
sqlPatch.Fields = make([]string, 0, n)
sqlPatch.Args = make([]interface{}, 0, n)
for i := 0; i < n; i++ {
fType := rType.Field(i)
fVal := rVal.Field(i)
tag := fType.Tag.Get(tagname)
// skip nil properties (not going to be patched), skip unexported fields, skip fields to be skipped for SQL
if fVal.IsNil() || fType.PkgPath != "" || tag == "-" {
continue
}
// if no tag is set, use the field name
if tag == "" {
tag = fType.Name
}
// and make the tag lowercase in the end
tag = strings.ToLower(tag)
sqlPatch.Fields = append(sqlPatch.Fields, tag+" = ?")
var val reflect.Value
if fVal.Kind() == reflect.Ptr {
val = fVal.Elem()
} else {
val = fVal
}
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
sqlPatch.Args = append(sqlPatch.Args, val.Int())
case reflect.String:
sqlPatch.Args = append(sqlPatch.Args, val.String())
case reflect.Bool:
if val.Bool() {
sqlPatch.Args = append(sqlPatch.Args, 1)
} else {
sqlPatch.Args = append(sqlPatch.Args, 0)
}
}
}
return sqlPatch
}
然后我可以像这样简单地调用它:
type Resource struct {
Description *string `json:"description,omitempty"`
Name *string `json:"name,omitempty"`
}
func main() {
var r Resource
json.Unmarshal([]byte(`{"description": "new description"}`), &r)
sqlPatch := SQLPatches(r)
data, _ := json.Marshal(sqlPatch)
fmt.Printf("%s\n", data)
}
你可以在Go Playground上检查它。我唯一看到的问题是,我为传递的结构体中的字段数量分配了两个片段,这可能是10,即使最终我只想修补一个属性,结果会分配比需要更多的内存... 有什么办法可以避免这种情况吗?