为了账号安全,请及时绑定邮箱和手机立即绑定

如何使用 `database/sql` 实现 PATCH?

如何使用 `database/sql` 实现 PATCH?

Go
www说 2022-10-17 19:31:08
假设您有一个由 SQL 数据库支持的基本 API (GET/POST/PATCH/DELETE)。PATCH 调用应该只更新用户发送的 JSON 有效负载中的字段,而不涉及任何其他字段。想象一下表(我们称它为sample)有id,string_a和string_b列,与之对应的结构如下所示:type Sample struct {  ID      int    `json:"id"`  StringA string `json:"stringA"`  StringB string `json:"stringB"`}假设用户{ "stringA": "patched value" }作为有效负载传入。json 将被解组为如下所示的内容:&Sample{ ID: 0, StringA: "patched value", StringB: "",}对于使用 的项目database/sql,您将编写查询来修补行,例如:// `id` is from the URL paramsquery := `UPDATE sample SET string_a=$1, string_b=$2 WHERE id=$3`row := db.QueryRow(query, sample.StringA, sample.StringB, id)...该查询将按string_a预期更新该列,但它也会将该string_b列更新为"",在这种情况下这是不希望的行为。本质上,我只是创建了一个 PUT 而不是 PATCH。我的直接想法是——好的,没关系,让我们用它strings.Builder来构建查询,只为那些具有非 nil/空值的语句添加一个 SET 语句。但是,在那种情况下,如果用户想要string_a清空,他们将如何实现呢?例如。用户使用有效负载进行 PATCH 调用{ "stringA": "" } 。这将被解组为:&Sample{  ID: 0,  StringA: "",  StringB: "",}我理论化的“查询构建器”会看着它并说“好的,这些都是 nil/空值,不要将它们添加到查询中”并且不会更新任何列,这又是不受欢迎的行为。我不确定如何编写我的 API 以及它以同时满足这两种情况的方式运行的 SQL 查询。有什么想法吗?
查看完整描述

2 回答

?
慕斯709654

TA贡献1840条经验 获得超5个赞

我认为较小查询的合理解决方案是动态构建UPDATE查询和绑定参数列表,同时使用识别更新内容和留空内容的逻辑处理有效负载。


根据我自己的经验,这是清晰易读的(如果重复,您始终可以迭代共享相同逻辑或使用反射并查看结构标记提示等的结构成员)。每次(我)为此编写通用解决方案的尝试都以非常复杂的矫枉过正,支持各种极端情况和端点之间的行为差异。


func patchSample(s Sample) {

    var query strings.Builder

    params := make([]interface{}, 0, 2)


    // TODO Check if patch makes sense (e.g. id is non-zero, at least one patched value provided, etc.


    query.WriteString("UPDATE sample SET")


    if s.StringA != "" {

        query.WriteString(" stringA = ?")

        params = append(params, s.StringA)

    }


    if s.StringB != "" {

        query.WriteString(" stringB = ?")

        params = append(params, s.StringB)

    }


    query.WriteString(" WHERE id = ?")

    params = append(params, s.ID)


    fmt.Println(query.String(), params)

    //_, err := db.Exec(query.String(), params...)

}


func main() {

    patchSample(Sample{1, "Foo", ""})

    patchSample(Sample{2, "", "Bar"})

    patchSample(Sample{3, "Foo", "Bar"})

}

编辑:如果""是修补的有效值,那么它需要与默认的空值区分开来。解决字符串问题的一种方法是使用指针,nil如果有效载荷中不存在值,则默认为:


type Sample struct {

    ID      int     `json:"id"`

    StringA *string `json:"stringA"`

    StringB *string `json:"stringB"`

}

然后修改条件以检查字段是否像这样发送:


if s.StringA != nil {

    query.WriteString(" stringA = ?")

    params = append(params, *s.StringA)

}

在操场上查看完整示例:https ://go.dev/play/p/RI7OsNEYrk6


查看完整回答
反对 回复 2022-10-17
?
婷婷同学_

TA贡献1844条经验 获得超8个赞

对于它的价值,我通过以下方式解决了这个问题:

  1. 将请求有效负载转换为通用map[string]interface{}.

  2. 实现一个查询构建器,该构建器循环遍历地图的键以创建查询。

我走这条路的部分原因是它符合我的所有要求,而且我不是特别喜欢有*strings 或*ints 躺在那里。

以下是查询构建器的样子:

func patchQueryBuilder(id string, patch map[string]interface{}) (string, []interface{}, error) {

    var query strings.Builder

    params := make([]interface{}, 0)


    query.WriteString("UPDATE some_table SET")

    for k, v := range patch {

        switch k {

        case "someString":

            if someString, ok := v.(string); ok {

                query.WriteString(fmt.Sprintf(" some_string=$%d,", len(params)+1))

                params = append(params, someString)

            } else {

                return "", []interface{}{}, fmt.Errorf("could not process some_string")

            }

        case "someBool":

            if someBool, ok := v.(bool); ok {

                query.WriteString(fmt.Sprintf(" some_bool=$%d,", len(params)+1))

                params = append(params, someBool)

            } else {

                return "", []interface{}{}, fmt.Errorf("could not process some_bool")

            }

        }

    }


    if len(params) > 0 {

        // Remove trailing comma to avoid syntax errors

        queryString := fmt.Sprintf("%s WHERE id=$%d RETURNING *", strings.TrimSuffix(query.String(), ","), len(params)+1)

        params = append(params, id)

        return queryString, params, nil

    } else {

        return "", []interface{}{}, nil

    }

}

请注意,我使用的是 PostgreSQL,所以我需要为查询提供编号参数,例如$1,这是params用于的。它也是从函数返回的,因此可以按如下方式使用:


// Build the patch query based on the payload

query, params, err := patchQueryBuilder(id, patch)

if err != nil {

    return nil, err

}


// Use the query/params and get output

row := tx.QueryRowContext(ctx, query, params...)


查看完整回答
反对 回复 2022-10-17
  • 2 回答
  • 0 关注
  • 167 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号