19

几天以来,我一直在为如何在 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"`
}

我期待一个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)

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,即使我最终可能只想修补一个属性,导致分配的内存比需要的多。 .知道如何避免这种情况吗?

4

3 回答 3

7

我最近有同样的问题。关于 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" }
]

这组指令应该更容易构建更新查询。

编辑

这是您获取 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 (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
于 2016-07-05T15:03:12.863 回答
2

结构标签只能通过反射才能看到,抱歉。

如果您不想使用反射(或者,我认为,即使您这样做了),我认为定义一个函数或方法将您的结构“编组”为可以轻松转换为逗号的东西,这类似于 Go -分隔的 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

如果你有很深的 structs-within-structs,那么在此基础上构建应该相当容易。而且,如果您更改为允许或鼓励位置参数的 SQL 引擎,例如$1而不是?,则很容易将该行为添加到SQLUpdates结构中,而无需更改任何使用它的代码。

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

于 2016-07-05T19:18:52.993 回答
1

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

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

然而,这并不总是被推荐的,特别是在通过减少冗余传输数据量来节省一些 CO 2的较大对象的情况下。今天,如果我需要为客户端启用 PATCH,我只需定义可以修补的内容 - 这给了我清晰度和最终结构。

请注意,我使用的是IETF 记录的 JSON Merge Patch实现。我认为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"`
    }

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

只发这个

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

我认为没有什么比这更简单、更有效了。没有反思,没有过度杀戮,读起来相当不错。

于 2021-09-13T10:51:30.243 回答