GoLang,REST,PATCH和构建UPDATE查询

时间:2016-07-05 14:48:41

标签: sql rest go http-patch

因为几天之后我一直在努力研究如何在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,但在现实生活中,结构比这里提供的示例大得多......

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

SOLUTION:

基于这里的答案,我能够提出这个抽象的解决方案。 SQLPatches方法从给定的struct构建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查看。我看到的唯一问题是我在传递的struct中分配了两个切片,其中的字段数量可能是10,即使我可能只想在最后修补一个属性,导致分配的内存超过需要的内存。 。知道如何避免这种情况吗?

2 个答案:

答案 0 :(得分:4)

我最近遇到了同样的问题。关于PATCH并四处寻找this article。它还引用了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" }
]

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

修改

这就是你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

答案 1 :(得分:1)

只有通过反思才能看到结构标记,抱歉。

如果你不想使用反射(或者,我认为,即使你这样做),我认为定义一个功能或方法是&#34; marshals&#34;您的结构可以很容易地转换为以逗号分隔的SQL更新列表,然后使用它。建立小东西来帮助解决你的问题。

例如:

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)
    }
}

您可以定义:

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
}

其中SQLUpdates类型如下所示:

$1

在此处查看工作(sorta):https://play.golang.org/p/IQAHgqfBRh

如果你有一个深层结构 - 结构,那么建立起来应该相当容易。如果您更改为允许或鼓励?而不是SQLUpdates等位置参数的SQL引擎,则可以轻松地将该行为添加到Exec结构而不更改任何使用它的代码。

为了让参数传递给Values(),您只需使用...运算符扩展{{1}}的输出。