从几天以来,我一直在努力研究如何在Go REST API中处理补丁请求,直到我找到了一个article about using pointers and omitempty tag,我已经填充了它,并且运行良好.很好,直到我意识到我仍然需要构建一个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)

PATCHomitempty标记的问题是,对象可能缺少多个属性,因此我不能只准备一个带有硬编码字段和占位符的语句...我必须动态地构建它.

我的问题来了:how can I build such 100 query dynamically?在最好的情况下,我需要一些解决方案来标识集合属性,获取它们的SQL个字段名称(可能来自标记),然后我应该能够构建UPDATE个查询.我知道我可以使用reflection来获取对象属性,但不知道如何获取它们的sql tag name,当然,如果可能的话,我希望避免在这里使用反射……或者我可以简单地判断每个属性,它不是nil,但在现实生活中, struct 比这里提供的示例要大得多……

有人能帮我搬这个吗?是否已经有人需要解决相同/相似的情况?

SOLUTION:

基于这里的答案,我能够想出这个抽象的解决方案.SQLPatches方法从给定的 struct 构建SQLPatch struct (因此没有特定的具体 struct ):

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点查.这里我看到的唯一问题是,我分配了两个片,在传递的 struct 中,字段的数量可能是10,即使我可能只想修补一个属性,最终导致分配的内存超过需要...Any idea how to avoid this?

推荐答案

我最近也遇到了同样的问题.关于帕奇和环顾四周发现了this article个.它还提到了RFC 5789,其中说:

PUT和PATCH请求之间的区别反映在服务器处理封闭实体以修改由Request-URI标识的资源的方式上.在PUT请求中,封闭的实体被认为是源站上存储的资源的修改版本,并且客户端请求替换存储的版本.With PATCH, however, the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version.补丁方法影响由Request-URI标识的资源,并且它还可能对其他资源具有副作用;即,可以通过应用补丁来创建新资源或修改现有资源.

例如:

[
    { "op": "test", "path": "/a/b/c", "value": "foo" },
    { "op": "remove", "path": "/a/b/c" },
    { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
    { "op": "replace", "path": "/a/b/c", "value": 42 },
    { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
    { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]

这组说明应该会使构建更新查询变得更容易.

EDIT

这就是你想要的,但你必须使用反射:

type Resource struct {
        Name        *string `json:"name,omitempty"        sql:"resource_id"`
        Description *string `json:"description,omitempty" sql:"description"`
}

sp := "sort of string"
r := Resource{Description: &sp}
rt := reflect.TypeOf(r) // reflect.Type
rv := reflect.ValueOf(r) // reflect.Value

for i := 0; i < rv.NumField(); i++ { // Iterate over all the fields
    if !rv.Field(i).IsNil() { // Check it is not nil

        // Here you would do what you want to having the sql tag.
        // Creating the query would be easy, however
        // not sure you would execute the statement

        fmt.Println(rt.Field(i).Tag.Get("sql")) // Output: description
    }
}   

我知道您不想使用反射,但正如您所 comments 的那样,这可能是一个比前一个更好的答案.

EDIT 2:

关于分配-请阅读这条有效GO约Data structures and Allocation的指南:

// Here you are allocating an slice of 0 length with a capacity of n
sqlPatch.Fields = make([]string, 0, n)
sqlPatch.Args = make([]interface{}, 0, n)

make(Type, Length, Capacity (optional))

考虑下面的例子:

// newly allocated zeroed value with Composite Literal 
// length: 0
// capacity: 0
testSlice := []int{}
fmt.Println(len(testSlice), cap(testSlice)) // 0 0
fmt.Println(testSlice) // []

// newly allocated non zeroed value with make   
// length: 0
// capacity: 10
testSlice = make([]int, 0, 10)
fmt.Println(len(testSlice), cap(testSlice)) // 0 10
fmt.Println(testSlice) // []

// newly allocated non zeroed value with make   
// length: 2
// capacity: 4
testSlice = make([]int, 2, 4)
fmt.Println(len(testSlice), cap(testSlice)) // 2 4
fmt.Println(testSlice) // [0 0]

在您的情况下,可能需要执行以下操作:

// Replace this
sqlPatch.Fields = make([]string, 0, n)
sqlPatch.Args = make([]interface{}, 0, n)

// With this or simple omit the capacity in make above
sqlPatch.Fields = []string{}
sqlPatch.Args = []interface{}{}

// The allocation will go as follow: length - capacity
testSlice := []int{} // 0 - 0
testSlice = append(testSlice, 1) // 1 - 2
testSlice = append(testSlice, 1) // 2 - 2   
testSlice = append(testSlice, 1) // 3 - 4   
testSlice = append(testSlice, 1) // 4 - 4   
testSlice = append(testSlice, 1) // 5 - 8

Go相关问答推荐

Makefile:现有文件上没有这样的文件或目录,不加载环境变量

Go PQ驱动程序无法使用默认架构进行查询

什么东西逃到了堆里?

如何创建在responseWriter.Write 上返回错误的http.ResponseWriter 模拟实例?

优化方式中所有可能组合的字符串相似度

Caddy服务器try 打开端口80而不是8090.

在 Go 中使用 Apache Arrow 按时间间隔对事件进行分区

当我的 go build 成功时,如何修复我的 docker build 失败? Dockerfile 包括 go mod 下载

gopacket:IP-in-IP 数据包上的解码层

如何将 base64 编码的公钥转换为 crypto.PublicKey 或 ecdsa.PublicKey

如何确定作为函数参数传递的指针是否正在被修改或副本是否正在被修改?

如何将具有嵌入式 struct 的 struct 展平为 json

当有多个同名包时如何在vscode中显示golang包完整路径?

有没有办法在 golang 中定义可索引类型?

Golang:每个键具有多个值的映射

gqlgen go,通过添加一个解析器来减少数据库调用

Dynamodb.ScanInput - 不能使用expr.Names()(类型 map[string]*string)作为类型 map[string]string

如何从应用程序调用谷歌云身份 API

如何使用通用字段初始化匿名struct数组

带有 *s3.S3 对象的 Golang 单元测试