GoLang,REST,PATCH和构建UPDATE查询

sha*_*yyx 16 sql rest go http-patch

几天以来,我一直在努力研究如何在Go REST API中继续使用PATCH请求,直到我找到一篇关于使用指针和omitempty标记文章,我已经填充并且工作正常.很好,直到我意识到我仍然需要构建一个UPDATESQL查询.

struct看起来像这样:

type Resource struct {
    Name        *string `json:"name,omitempty"        sql:"resource_id"`
    Description *string `json:"description,omitempty" sql:"description"`
}
Run Code Online (Sandbox Code Playgroud)

我期待一个PATCH /resources/{resource-id}包含这样一个请求体的请求:

{"description":"Some new description"}
Run Code Online (Sandbox Code Playgroud)

在我的处理程序中,我将以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
Run Code Online (Sandbox Code Playgroud)

现在,对于正常UPDATE(PUT请求)我会这样做(简化):

stmt, _ := db.Prepare(`UPDATE resources SET description = ?, name = ? WHERE resource_id = ?`)
res, _ := stmt.Exec(resource.Description, resource.Name, resourceID)
Run Code Online (Sandbox Code Playgroud)

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

这里有我的问题:如何UPDATE动态构建此类查询?在最好的情况下,我需要一些解决方案来识别设置属性,获取他们的SQL字段名称(可能来自标签),然后我应该能够构建UPDATE查询.我知道我可以使用反射获取对象属性但不知道热得到他们的sql标签名称,当然我想避免在这里使用反射如果可能...或者我可以简单地检查每个属性它不是nil,但在现实生活中,结构比这里提供的示例大得多......

有人可以帮我这个吗?有人已经有必要解决相同/类似的情况吗?

解:

基于这里的答案,我能够提出这个抽象的解决方案.该SQLPatches方法SQLPatch从给定的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
}
Run Code Online (Sandbox Code Playgroud)

然后我可以简单地这样称呼它:

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)
}
Run Code Online (Sandbox Code Playgroud)

您可以在Go Playground查看.我看到的唯一问题是我在传递的struct中分配了两个切片,其中可能是10个,尽管我可能只想在最后修补一个属性,导致分配的内存超过需要的内存. .任何想法如何避免这种情况?

sha*_*yyx 7

好吧,我认为我在 2016 年使用的解决方案对于更多过度设计的问题来说是相当过度设计的,而且完全没有必要。这里提出的问题非常笼统,但是我们正在构建一个解决方案,能够根据请求中发送的 JSON 对象或查询参数和/或标头自行构建 SQL 查询。并且尽可能通用。

如今,我认为最好的解决方案是避免使用 PATCH,除非确实有必要。即使如此,您仍然可以使用 PUT 并用来自客户端的已修补属性替换整个资源 - 即不向客户端提供将任何 PATCH 请求发送到您的服务器并自行处理部分更新的选项/可能性

然而,并不总是建议这样做,特别是在对象较大的情况下,通过减少冗余传输数据量来节省一些 C0 2 。今天每当我需要为客户端启用补丁时,我只需定义可以修补的内容 - 这使我更加清晰和最终的结构。

请注意,我正在使用IETF 记录的 JSON 合并补丁实现。我认为JSON Patch(也由 IETF 记录)是多余的,因为假设我们可以通过拥有一个端点来替换整个 REST API JSON Patch,并让客户端通过允许的操作来控制资源。JSON Patch我还认为在服务器端实现此类要复杂得多。我能想到的使用这种实现的唯一用例是,如果我在文件系统上实现 REST API...

所以结构可以定义为我的OP:

    type ResourcePatch struct {
        ResourceID  some.UUID `json:"resource_id"`
        Description *string `json:"description,omitempty"`
        Name        *string `json:"name,omitempty"`
    }
Run Code Online (Sandbox Code Playgroud)

在处理程序函数中,我会将 ID 从路径解码到 ResourcePatch 实例中,并将请求正文中的 JSON 解组到其中。

只发送这个

{"description":"Some new description"}
Run Code Online (Sandbox Code Playgroud)

PATCH /resources/<UUID>

我最终应该得到这个对象:

ResourcePatch
    * ResourceID {"UUID"}
    * Description {"Some new description"}
Run Code Online (Sandbox Code Playgroud)

现在神奇的是:使用简单的逻辑来构建查询和执行参数。对于某些人来说,对于较大的 PATCH 对象来说,这可能看起来很乏味、重复或不干净,但我对此的回答是:如果您的 PATCH 对象包含超过 50% 的原始资源属性(或者只是太多而不适合您的喜好),请使用 PUT并期望客户端发送(并替换)整个资源

它可能看起来像这样:

{"description":"Some new description"}
Run Code Online (Sandbox Code Playgroud)

我认为没有什么比这更简单、更有效的了。没有反思,没有过度杀戮,读起来很好。


Dav*_*eri 6

我最近有同样的问题。关于PATCH并环顾四周发现了这篇文章。它还引用了RFC 5789,其中说:

PUT和PATCH请求之间的差异反映在服务器处理封闭实体以修改由Request-URI标识的资源的方式上。在PUT请求中,封闭的实体被视为原始服务器上存储的资源的修改版本,并且客户端正在请求替换存储的版本。但是,对于PATCH,封闭的实体包含一组指令,这些指令描述了应如何修改当前驻留在源服务器上的资源以产生新版本。PATCH方法影响由Request-URI标识的资源,并且可能对其他资源也有副作用。也就是说,可以通过应用PATCH来创建新资源或修改现有资源。

例如:

[
    { "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" }
]
Run Code Online (Sandbox Code Playgroud)

这组说明应该使构建更新查询更加容易。

编辑

这是获取sql标记的方法,但是必须使用反射:

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
    }
}   
Run Code Online (Sandbox Code Playgroud)

我了解您不希望使用反射,但是在您发表评论时,这可能是比上一个更好的答案。

编辑2:

关于分配-请阅读以下有效数据结构和分配指南

// 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)
Run Code Online (Sandbox Code Playgroud)

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]
Run Code Online (Sandbox Code Playgroud)

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

// 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
Run Code Online (Sandbox Code Playgroud)