因为几天之后我一直在努力研究如何在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
现在,对于正常的UPDATE
(PUT
请求),我会这样做(简化):
stmt, _ := db.Prepare(`UPDATE resources SET description = ?, name = ? WHERE resource_id = ?`)
res, _ := stmt.Exec(resource.Description, resource.Name, resourceID)
PATCH
和omitempty
标记的问题是对象可能缺少多个属性,因此我不能只使用硬编码字段和占位符准备语句...我将不得不动态构建它
这就是我的问题:如何动态构建这样的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,即使我可能只想在最后修补一个属性,导致分配的内存超过需要的内存。 。知道如何避免这种情况吗?
答案 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}}的输出。