GoLang、REST、PATCH 和构建 UPDATE 查询

GoLang, REST, PATCH and building an UPDATE query

几天以来,我一直在努力研究如何在 Go REST API 中处理 PATCH 请求,直到我找到一个 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

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

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

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

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

有人可以帮我解决这个问题吗?有人必须解决 same/similar 的情况吗?

解决方案:

根据此处的答案,我得出了这个抽象的解决方案。 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,即使我可能最后只想修补一个 属性 导致分配更多内存超出需要... 知道如何避免这种情况吗?

我最近遇到了同样的问题。关于 PATCH 并环顾四周发现 this article. It also makes references to the RFC 5789 上面写着:

The difference between the PUT and PATCH requests is reflected in the way the server processes the enclosed entity to modify the resource identified by the Request-URI. In a PUT request, the enclosed entity is considered to be a modified version of the resource stored on the origin server, and the client is requesting that the stored version be replaced. 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. The PATCH method affects the resource identified by the Request-URI, and it also MAY have side effects on other resources; i.e., new resources may be created, or existing ones modified, by the application of a 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" }
]

这组说明应该可以更轻松地构建更新查询。

编辑

这就是你想要的方式 obtain sql tags 但你将不得不使用反射:

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 指南关于 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 的,"marshals" 你的结构变成了一些东西可以很容易地变成逗号分隔的 SQL 更新列表,然后使用它。构建小东西来帮助解决您的问题。

例如给定:

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

如果您有深层的结构内结构,那么在此基础上构建应该相当容易。如果您更改为允许或鼓励像 </code> 而不是 <code>? 这样的位置参数的 SQL 引擎,则很容易将该行为添加到 SQLUpdates 结构而无需更改任何使用它的代码。

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

好吧,我认为我在 2016 年使用的解决方案对于 over-engineered 更多 over-engineered 问题来说是相当 over-engineered 并且完全没有必要。这里提出的问题非常笼统,但是我们正在构建一个解决方案,该解决方案能够自行构建其 SQL 查询并基于 JSON object 或查询参数 and/or Headers 在请求中发送。并且尽可能通用。

如今,我认为最好的解决方案是避免 PATCH,除非确实有必要。即便如此,您仍然可以使用 PUT 并将整个资源替换为已经来自客户端的补丁 property/ies - 即不给客户端 option/possibility 来向您的服务器发送任何 PATCH 请求并处理 自己部分更新

然而,并不总是推荐这样做,特别是在 更大 object 的情况下,通过减少来节省一些 C02冗余传输数据量。今天每当我需要为客户端启用 PATCH 时,我只需定义可以修补的内容 - 这让我清晰明了并给出了最终结构。

请注意,我使用的是 IETF documented JSON Merge Patch implementation. I consider that of JSON Patch (also documented by IETF) 冗余,因为假设我们可以用一个 JSON Patch 端点替换整个 REST API 并让客户端控制通过允许的操作获取资源。我还认为在服务器端实现这样的 JSON Patch 要复杂得多。我唯一能想到的 use-case 是如果我在文件系统上实现 REST API...

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

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

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

只发送这个

{"description":"Some new description"}

PATCH /resources/<UUID>

我应该这样结束 object:

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

现在神奇的是:使用简单的逻辑来构建查询和执行参数。对于某些人来说,更大的 PATCH objects 可能看起来乏味、重复或不干净,但我对此的答复是:如果你的 PATCH object 包含超过 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
    }

我认为没有比这更简单、更有效的了。没有反映,没有over-kills,读起来还不错