GoLang,REST,PATCH和构建UPDATE查询

25

最近几天我一直在为如何在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

现在,对于普通的UPDATEPUT请求),我会这样做(简化版):
stmt, _ := db.Prepare(`UPDATE resources SET description = ?, name = ? WHERE resource_id = ?`)
res, _ := stmt.Exec(resource.Description, resource.Name, resourceID)

使用PATCHomitempty标签的问题在于对象可能缺少多个属性,因此我不能只准备一个带有硬编码字段和占位符的语句......我必须动态构建它。
那么问题来了:我该如何动态构建这样的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,即使最终我只想修补一个属性,结果会分配比需要更多的内存... 有什么办法可以避免这种情况吗?

所以,现在是2021年。有没有更好的解决方案来完成这项任务? - GHopper
@GHopper 你好,感谢您的评论。我在这里添加了自己的答案,您也可以在那里找到您的问题的答案 :-) - shadyyx
3个回答

8
我最近遇到了关于PATCH的问题,寻找资料时发现这篇文章。它还引用了RFC 5789,其中提到:
PUT和PATCH请求的区别反映在服务器处理封装实体以修改由Request-URI标识的资源时的方式上。在PUT请求中,封装的实体被视为存储在源服务器上的资源的修改版本,并且客户端请求替换存储的版本。然而,使用PATCH,封装的实体包含描述应如何修改当前驻留在源服务器上的资源以生成新版本的指令集。 PATCH方法会影响由Request-URI标识的资源,并且也可能对其他资源产生副作用;即,应用程序可以通过PATCH创建新资源或修改现有资源。

e.g:

[
    { "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" }
]

这组指令应该会让更新查询更容易构建。 编辑 这是您将获取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
    }
}   

我知道您不想使用反射,但是这个答案可能比之前的答案更好,因为您已经评论过了。
编辑2:
关于分配 - 请阅读Effective Go指南中有关数据结构和分配的准则
// 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 (可选))

请考虑以下示例:

// 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

我已经阅读了这篇文章。在我们的情况下,PATCH 操作将始终仅替换提供的属性集合,因此我决定从请求体中省略冗余属性,考虑到{property:value}应该足够了。RFC 对于[description of changes]的描述并不具体,因此我无法相信任何人说“这种方式是错误的”或“只有这种方式才是正确的”。尽管如此,你的回答并没有帮助我解决问题。 - shadyyx
@shadyyx,我更新了答案,这会给你一个概念,但它使用了反射。 - David Lavieri
@shadyyx 根据你的分配问题,我更新了答案。 - David Lavieri

7

好的,我认为我在2016年使用的解决方案对于更加复杂的问题来说过于复杂了,并且完全没有必要。这里提出的问题非常普遍,然而我们正在构建一个能够自行构建其SQL查询的解决方案,基于JSON对象或查询参数和/或发送请求的标头。并使其具有尽可能通用性。

现在我认为最好的解决方案是避免使用PATCH,除非真正必要。即使是这样,您仍然可以使用PUT并用来自客户端的已打补丁的属性/ies替换整个资源-即不给予客户端发送任何PATCH请求到您的服务器并且处理其自身的部分更新的选项/可能性。

然而,这并不总是推荐的,特别是在需要保存一些C02通过减少冗余传输数据量的情况下。每当今天我需要为客户端启用PATCH时,我只需定义可以打补丁的内容-这为我提供了清晰度和最终结构。

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

因此,该结构可以按照我的OP进行定义:

    type ResourcePatch struct {
        ResourceID  some.UUID `json:"resource_id"`
        Description *string `json:"description,omitempty"`
        Name        *string `json:"name,omitempty"`
    }

在处理函数中,我会从路径中解码出ID,并将JSON反序列化为ResourcePatch实例。
{"description":"Some new description"}

要进行 PATCH /resources/<UUID> 操作

最终应该得到这个对象:

ResourcePatch
    * ResourceID {"UUID"}
    * Description {"Some new description"}

现在让我们来看一下“魔法”:使用简单的逻辑构建查询和执行参数。对于一些人来说,这可能会感觉繁琐、重复或者不够干净,尤其是对于较大的 PATCH 对象,但我想说的是:如果您的 PATCH 对象包含超过原始资源属性的50%(或者您认为属性过多),请使用 PUT 方法,并期望客户端发送(并替换)整个资源。

实现起来可能像这样:

    func (s Store) patchMyResource(r models.ResourcePatch) error {
        q := `UPDATE resources SET `
        qParts := make([]string, 0, 2)
        args := make([]interface{}, 0, 2)

        if r.Description != nil {
            qParts = append(qParts, `description = ?`)
            args = append(args, r.Description)
        }

        if r.Name != nil {
            qParts = append(qParts, `name = ?`)
            args = append(args, r.Name)
        }

        q += strings.Join(qParts, ',') + ` WHERE resource_id = ?`
        args = append(args, r.ResourceID)

        _, err := s.db.Exec(q, args...)

        return err
    }

我认为没有什么比这更简单有效了。没有反射,没有过度设计,读起来十分流畅。


3

抱歉,结构标签仅通过反射可见。

如果您不想使用反射(或者我认为即使您想使用),那么定义一个函数或方法来将您的结构体“编组”成可以轻松转换为逗号分隔的SQL更新列表的东西,然后使用它是类似于Go的。构建小工具来解决问题。

例如给出:

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

您可以定义以下内容:

func (r Resource) SQLUpdates() SQLUpdates {
    var s SQLUpdates
    if (r.Name != nil) {
        s.add("resource_id", *r.Name)
    }
    if (r.Description != nil) {
        s.add("description", *r.Description)
    }
}

其中,类型SQLUpdates可能看起来像这样:

type SQLUpdates struct {
    assignments []string
    values []interface{}
}
func (s *SQLUpdates) add(key string, value interface{}) {
    if (s.assignments == nil) {
        s.assignments = make([]string, 0, 1)
    }
    if (s.values == nil) {
        s.values = make([]interface{}, 0, 1)
    }
    s.assignments = append(s.assignments, fmt.Sprintf("%s = ?", key))
    s.values = append(s.values, value)
}
func (s SQLUpdates) Assignments() string {
    return strings.Join(s.assignments, ", ")
}
func (s SQLUpdates) Values() []interface{} {
    return s.values
}

在这里可以看到它的工作原理(有点):https://play.golang.org/p/IQAHgqfBRh

如果您有深层嵌套的结构体,那么构建它就应该相当容易。如果您切换到允许或鼓励使用位置参数(如$1而不是?)的SQL引擎,则可以将此行为轻松添加到仅SQLUpdates结构中,而无需更改使用它的任何代码。

为了将参数传递给Exec,您只需使用...运算符扩展Values()的输出即可。


当然,你也可以在SQLUpdates.add()顶部添加 if value == nil { return; },那么你通常可以将你的“编组”方法编写为对add()调用的序列。 - Jesse Amano
这基本上与我昨天在发布问题等待答案时想到的一样 :-) 你的答案代码比我做的更有结构,我很喜欢!我会稍微重构一下以适应我们项目的结构,并在一切正常工作后接受答案!谢谢! - shadyyx
请检查我的额外解决方案和最终问题 :-) 谢谢顺便说一句! - shadyyx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接