golang自动从结构体生成SQL查询的强大构建器插件patcher的使用

Golang自动从结构体生成SQL查询的强大构建器插件Patcher的使用

Patcher是一个Go库,提供了一种从结构体生成SQL补丁的简单方法。这个库的诞生源于为数据库生成补丁的需求:当向结构体添加新字段时,会导致代码库中出现大量新的if检查。Patcher旨在通过自动生成SQL补丁来解决这个问题。

什么是Patcher?

  • 自动SQL生成:自动从结构体生成SQL UPDATE查询,减少手动编写和维护SQL语句的需求
  • 代码简化:减少了处理不同结构体字段所需的样板代码和if-else条件,使代码更简洁易维护
  • 结构体差异:允许将一个结构体的更改注入到另一个结构体,并根据差异生成更新脚本
  • JOIN支持:通过创建实现Joiner接口的结构体来生成SQL JOIN

为什么使用Patcher?

  • 节省时间:自动从结构体生成SQL查询,减少手动编写和维护SQL语句的时间
  • 减少错误:根据结构体字段自动生成SQL查询,消除结构体字段变更时需要手动更新查询的风险
  • 简化代码:减少处理不同结构体字段所需的样板代码和if-else条件
  • 简化数据同步:通过结构体差异比较生成更新脚本
  • 支持JOIN:通过Joiner接口简化多表关联查询
  • 灵活配置:提供自定义SQL生成过程的选项
  • 易于集成:可轻松集成到现有项目中
  • 开源:基于Apache 2.0许可证

使用示例

不使用Patcher的情况

package main

import (
	"database/sql"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"github.com/gorilla/mux"
)

type User struct {
	ID    int     `json:"id"`
	Name  *string `json:"name"`
	Email *string `json:"email"`
}

func updateUser(db *sql.DB, user User) error {
	query := "UPDATE users SET"
	var updates []string
	var args []any

	if user.Name != nil {
		updates = append(updates, "name = ?")
		args = append(args, *user.Name)
	}
	if user.Email != nil {
		updates = append(updates, "email = ?")
		args = append(args, *user.Email)
	}

	if len(updates) == 0 {
		return errors.New("no fields to update")
	}

	query += " " + strings.Join(updates, ", ") + " WHERE id = ?"
	args = append(args, user.ID)

	_, err := db.Exec(query, args...)
	return err
}

func patchHandler(w http.ResponseWriter, r *http.Request) {
	db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer db.Close()

	vars := mux.Vars(r)
	userIDStr := vars["id"]
	userID, err := strconv.Atoi(userIDStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	user := User{ID: userID}
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if err := updateUser(db, user); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/users/{id}", patchHandler).Methods("PATCH")

	server := &http.Server{
		Addr:              ":8080",
		Handler:           r,
		ReadHeaderTimeout: 10 * time.Second,
	}

	if err := server.ListenAndServe(); err != nil {
		panic(err)
	}
}

使用Patcher的情况

package main

import (
	"database/sql"
	"encoding/json"
	"net/http"
	"strconv"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"github.com/gorilla/mux"
	"github.com/jacobbrewer1/patcher"
)

type User struct {
	ID    *int    `db:"id" json:"id,omitempty"`
	Name  *string `db:"name" json:"name,omitempty"`
	Email *string `db:"email" json:"email,omitempty"`
}

type UserWhere struct {
	ID *int `db:"id"`
}

func NewUserWhere(id int) *UserWhere {
	return &UserWhere{ID: &id}
}

func (u *UserWhere) Where() (sqlStr string, sqlArgs []any) {
	return "id = ?", []any{*u.ID}
}

func patchHandler(w http.ResponseWriter, r *http.Request) {
	db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer db.Close()

	vars := mux.Vars(r)
	idStr := vars["id"]
	parsedID, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	user := new(User)
	if err := json.NewDecoder(r.Body).Decode(user); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	user.ID = &parsedID
	condition := NewUserWhere(parsedID)

	sqlStr, args, err := patcher.GenerateSQL(
		user,
		patcher.WithTable("users"),
		patcher.WithWhere(condition),
	)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	_, execErr := db.Exec(sqlStr, args...)
	if execErr != nil {
		http.Error(w, execErr.Error(), http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/users/{id}", patchHandler).Methods("PATCH")

	server := &http.Server{
		Addr:              ":8080",
		Handler:           r,
		ReadHeaderTimeout: 5 * time.Second,
	}

	if err := server.ListenAndServe(); err != nil {
		panic(err)
	}
}

基本用法

基本示例

package main

import (
	"encoding/json"
	"fmt"

	"github.com/jacobbrewer1/patcher"
)

type Person struct {
	ID   *int    `db:"-"`
	Name *string `db:"name"`
}

type PersonWhere struct {
	ID *int `db:"id"`
}

func NewPersonWhere(id int) *PersonWhere {
	return &PersonWhere{
		ID: &id,
	}
}

func (p *PersonWhere) Where() (string, []any) {
	return "id = ?", []any{*p.ID}
}

func main() {
	const jsonStr = `{"id": 1, "name": "john"}`

	person := new(Person)
	if err := json.Unmarshal([]byte(jsonStr), person); err != nil {
		panic(err)
	}

	condition := NewPersonWhere(*person.ID)

	sqlStr, args, err := patcher.GenerateSQL(
		person,
		patcher.WithTable("people"),
		patcher.WithWhere(condition),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(sqlStr)
	fmt.Println(args)
}

输出SQL:

UPDATE people
SET name = ?
WHERE (1 = 1)
  AND (
    id = ?
    )

参数:

["john", 1]

结构体差异比较

package main

import (
	"fmt"

	"github.com/jacobbrewer1/patcher"
)

type Something struct {
	Number       int
	Text         string
	PrePopulated string
	NewText      string
}

func main() {
	s := Something{
		Number:       5,
		Text:         "Hello",
		PrePopulated: "PrePopulated",
	}

	n := Something{
		Number:  6,
		Text:    "Old Text",
		NewText: "New Text",
	}

	// 将n的变更应用到s
	if err := patcher.LoadDiff(&s, &n); err != nil {
		panic(err)
	}

	fmt.Println(s.Number)
	fmt.Println(s.Text)
	fmt.Println(s.PrePopulated)
	fmt.Println(s.NewText)
}

输出:

6
Hello
PrePopulated
New Text

从差异生成SQL更新

package main

import (
	"fmt"

	"github.com/jacobbrewer1/patcher"
)

type Something struct {
	Number       int
	Text         string
	PrePopulated string
	NewText      string
}

type SomeWhere struct {
	id int
}

func NewSomeWhere(id int) *SomeWhere {
	return &SomeWhere{id: id}
}

func (s *SomeWhere) Where() (string, []any) {
	return "id = ?", []any{s.id}
}

func main() {
	s := Something{
		Number:       5,
		Text:         "Old Text",
		PrePopulated: "PrePopulated",
		NewText:      "New Text",
	}

	n := Something{
		Number:       5,
		Text:         "Old Text",
		PrePopulated: "PrePopulatedDifferent",
		NewText:      "New Text",
	}

	wherer := NewSomeWhere(5)

	// 从差异创建SQL补丁
	patch, err := patcher.NewDiffSQLPatch(
		&s,
		&n,
		patcher.WithTable("table_name"),
		patcher.WithWhere(wherer),
	)
	if err != nil {
		panic(err)
	}

	sqlStr, sqlArgs, err := patch.GenerateSQL()
	if err != nil {
		panic(err)
	}

	fmt.Println(sqlStr)
	fmt.Println(sqlArgs)
}

输出SQL:

UPDATE table_name
SET pre_populated = ?
WHERE (1 = 1)
  AND (
    id = ?
    )

参数:

["PrePopulatedDifferent", 5]

安装

使用以下命令安装Patcher库:

go get github.com/jacobbrewer1/patcher

配置选项

LoadDiff选项

  • includeZeroValues: 设置为true以在差异中包含零值
  • includeNilValues: 设置为true以在差异中包含nil值

GenerateSQL选项

  • WithTable(tableName string): 指定SQL查询的表名
  • WithWhere(whereClause Wherer): 提供SQL查询的WHERE子句
    • 可以传递实现WhereTyper接口的结构体以在WHERE子句中使用OR
  • WithJoin(joinClause Joiner): 添加JOIN子句到SQL查询
  • includeZeroValues: 设置为true以在补丁中包含零值
  • includeNilValues: 设置为true以在补丁中包含nil值

贡献

欢迎贡献!请按照以下步骤进行贡献:

  1. Fork仓库
  2. 为你的功能或错误修复创建新分支
  3. 为你的变更编写测试
  4. 运行测试确保一切正常
  5. 提交拉取请求

运行测试:

go test ./...

更多关于golang自动从结构体生成SQL查询的强大构建器插件patcher的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang自动从结构体生成SQL查询的强大构建器插件patcher的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang 结构体到 SQL 查询构建器 - Patcher 使用指南

Patcher 是一个强大的 Go 语言工具,可以自动从结构体生成 SQL 查询语句,简化数据库操作。下面我将详细介绍如何使用 Patcher。

安装 Patcher

首先安装 Patcher 工具:

go get -u github.com/huandu/go-patcher/patcher

基本使用示例

假设我们有以下用户结构体:

type User struct {
    ID        int64  `db:"id"`
    Name      string `db:"name"`
    Email     string `db:"email"`
    Age       int    `db:"age"`
    CreatedAt time.Time `db:"created_at"`
}

1. 生成 SELECT 查询

func GetUsersByAge(db *sql.DB, minAge, maxAge int) ([]User, error) {
    var users []User
    
    q := patcher.NewSelectQuery(&User{})
    q.Where("age BETWEEN ? AND ?", minAge, maxAge)
    q.OrderBy("age DESC")
    q.Limit(10)
    
    query, args := q.Build()
    rows, err := db.Query(query, args...)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age, &user.CreatedAt); err != nil {
            return nil, err
        }
        users = append(users, user)
    }
    
    return users, nil
}

2. 生成 INSERT 查询

func CreateUser(db *sql.DB, user *User) error {
    q := patcher.NewInsertQuery(user)
    query, args := q.Build()
    
    result, err := db.Exec(query, args...)
    if err != nil {
        return err
    }
    
    id, err := result.LastInsertId()
    if err != nil {
        return err
    }
    
    user.ID = id
    return nil
}

3. 生成 UPDATE 查询

func UpdateUserEmail(db *sql.DB, id int64, newEmail string) error {
    q := patcher.NewUpdateQuery(&User{})
    q.Set("email", newEmail)
    q.Where("id = ?", id)
    
    query, args := q.Build()
    _, err := db.Exec(query, args...)
    return err
}

4. 生成 DELETE 查询

func DeleteUser(db *sql.DB, id int64) error {
    q := patcher.NewDeleteQuery(&User{})
    q.Where("id = ?", id)
    
    query, args := q.Build()
    _, err := db.Exec(query, args...)
    return err
}

高级功能

条件构建

func SearchUsers(db *sql.DB, name string, minAge, maxAge int) ([]User, error) {
    q := patcher.NewSelectQuery(&User{})
    
    if name != "" {
        q.Where("name LIKE ?", "%"+name+"%")
    }
    
    if minAge > 0 {
        q.Where("age >= ?", minAge)
    }
    
    if maxAge > 0 {
        q.Where("age <= ?", maxAge)
    }
    
    query, args := q.Build()
    // 执行查询...
}

批量插入

func BatchCreateUsers(db *sql.DB, users []*User) error {
    q := patcher.NewBatchInsertQuery(users...)
    query, args := q.Build()
    
    _, err := db.Exec(query, args...)
    return err
}

自定义表名

默认情况下,Patcher 使用结构体名的小写形式作为表名。你可以通过实现 TableName() 方法自定义:

func (u *User) TableName() string {
    return "app_users"
}

性能优化建议

  1. 对于频繁执行的查询,可以缓存构建好的 SQL 语句
  2. 批量操作时使用事务提高性能
  3. 复杂的 JOIN 查询建议手动编写 SQL

总结

Patcher 提供了从结构体自动生成 SQL 查询的强大功能,可以显著减少样板代码。它特别适合 CRUD 操作,但对于复杂查询,可能需要结合原生 SQL 使用。

希望这个指南能帮助你快速上手 Patcher!如需更高级用法,可以参考官方文档。

回到顶部