19

数日以来、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)

PATCHandタグの問題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で確認できます。ここで私が目にする唯一の問題は、渡された構造体のフィールドの量で両方のスライスを割り当てることです。これは、最終的に 1 つのプロパティにパッチを適用したいだけで、必要以上のメモリを割り当てることになる場合があります.. .これを回避する方法はありますか?

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:

割り当てについて -データ構造と割り当てについての効果的なゴーのこのガイドラインを読んでください:

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

構造内に深い構造体がある場合、これを基に構築するのはかなり簡単です。$1また、 の代わりに のような位置引数を許可または推奨する SQL エンジンに変更した場合、?その動作を構造体だけに追加するのは簡単SQLUpdatesです。それを使用するコードを変更する必要はありません。

に渡す引数を取得するために、 の出力を演算子Execで展開するだけです。Values()...

于 2016-07-05T19:18:52.993 に答える