Golang实现支持1000种查询模板的REST API开发指南

Golang实现支持1000种查询模板的REST API开发指南 如何在 Golang REST API 中管理大约 1000 个 SQL 查询?

我的 SQL 经验处于使用 PostgreSQL 的中上水平。目前我使用的工具既支持原生 SQL,也支持类似 ORM 的方式直接访问数据库。使用 ORM 的目的是简化操作,但对于复杂查询反而使其更加困难。因此我倾向于使用原生 SQL,并且不想学习 GORM 或其他工具。

在 Golang 方面,我的经验还处于初级入门水平。我曾用大约 5 个查询完成了一个简单的 REST API,这是可以管理的。

所有 Golang REST API 示例都停留在"Hello World"级别。但我需要一种能够处理数百个结构体和查询的方法。要易于编码、易于理解和维护。到目前为止,我找到了三种实现方式:

  1. 使用模板或类似方式存储查询。https://github.com/gchaincl/dotsql
  2. 使用 1000 个包。但这对我来说似乎难以管理。
  3. 将查询与结构体一起存储在查找用的 PostgreSQL 数据库中。获取所需查询并执行查询。这样易于维护,但会增加可能影响速度的额外层。

除了这些想法,我还尝试过动态方法但没有成功。

我想知道是否有人遇到过这种情况并能分享一些想法和建议?

先谢谢了!


更多关于Golang实现支持1000种查询模板的REST API开发指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html

18 回复

我是否应该把你的回答理解为:“使用1000个包”?

更多关于Golang实现支持1000种查询模板的REST API开发指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,它有效! 到目前为止我们还没有检测到延迟。

是的,我做到了!

这涉及到大约四百个SQL调用公式。由于公式可能随时更改,因此利用这种数据库方法非常有用。

编程愉快!

你的 SQL 可能包含一些重复部分,建议使用 SQL 构建器包(例如 go-sqlbuilder)并在可能的情况下重用代码。

Marco_Azevedo:

是的,我做到了!

大约有四百个SQL调用公式。这些公式可能随时会更改,因此利用这种数据库方法非常有用。

很高兴听到我的想法在现实中可行。没有因为数据库造成任何延迟吗?

既然你说你是Go和SQL的初学者,我在想你所想要做的和你实际需要做的可能是完全不同的两件事。

提供更多细节会很有帮助。你是在尝试将现有系统REST化吗?能否提供一些API端点及相关查询的示例?

这样来看,你有多大可能会修改SQL而不改动业务逻辑?

  • 如果通常情况是两者都需要修改,那么将它们放在一起可能更合适
  • 如果你认为会频繁修改SQL但无需调整业务逻辑,那么将SQL存储在外部文件会更有吸引力

希望这个解释能帮到你。

Cecil_New:

只有当处理逻辑极其规范时,我才会考虑将其存储在端点逻辑之外;"极其规范"指的是某个端点服务于客户端的多个表单/页面,并且组合规则简单规范。

确实如此。有些查询在许多页面中被重复使用,但在前端以不同方式呈现。在这种情况下,将其存储在外部有什么好处?

你好 Sibert,

我建议在数据库中创建一个查询目录。也就是说……在数据库中创建一个表来存储SQL语句,每个SQL都有一个唯一ID来标识,而不是在代码中放置上千行的SQL代码。

这个ID可以作为端点的参数,甚至是资源URI(这取决于业务规则)。这样你就可以用Go开发一个机制来选择相应的记录。

请告诉我这个解决方案是否合理。

祝好!

普遍的做法是从将所有内容放在一个包中开始。"端点"将解析为包中的函数或方法。随着您开发支持所有1000个端点,应该清楚如何重新组织并将其拆分为多个包。例如,可能有一个包用于所有客户端点。如果您使用nginx或类似工具来引导流量,那么在不破坏RESTful API的情况下拆分包会更简单。即使没有nginx,这也不是那么困难。

希望这对您有所帮助!

Marco_Azevedo:

我将在数据库中创建一个查询目录。意思是…在数据库中创建一张表来存储SQL语句,用唯一ID来标识SQL,而不是在代码中放置上千行SQL。

将查询存储在查找数据库中是我考虑的方案之一。优点是无需部署和重启API即可添加和更新查询。这将真正简化API的维护工作。

缺点在于增加了额外的(延迟?)层,并且API无法轻松迁移到其他服务器。

有人对此有任何看法,或者已经尝试过使用数据库存储查询吗?

colega:

你的 SQL 可能包含一些重复部分,建议使用 SQL 构建器包(如 go-sqlbuilder)并在可能的情况下复用代码。

sb.Select("id", "name", sb.As("COUNT(*)", "c"))

这难道不算是某种 ORM 吗?

Cecil_New:

当你开发支持所有 1000 个端点时,应该会清楚如何重组代码并将其拆分成多个包。

所以你更倾向于使用包?

Cecil_New:

这样来看,你有多大可能会修改 SQL 而不改动业务逻辑?

  • 如果通常情况是两者都需要修改,那么不妨将它们放在一起
  • 如果你认为会频繁修改 SQL 但无需调整业务逻辑,那么将 SQL 存储在外部就有一定吸引力

日常工作中会频繁更新和添加查询语句(例如修复缺陷、新增功能等)。而 REST 接口的业务逻辑相对基础,可能每月甚至每年才需要调整几次。

这样一来就剩下两种选择:数据库或模板。哪种方式更合适?需要考虑执行效率和维护便利性。

Sibert:

我不认为自己是SQL新手。

我无意冒犯您。您最初发帖的第一行就提到您的SQL熟练程度处于"基础"水平。"基础"或"初学者"对我来说意思相同。不过这并不重要。

您正在尝试对现有系统进行REST化改造,那么您是否已经开发了API?开发/组织您的API不仅有助于组织结构和查询,还能减少所需的查询数量;可能远少于1000个。您甚至可能发现REST并非最佳选择,而GraphQL方法更为合适。

遗憾的是,除了强调您"必须"以某种方式组织1000个独立查询之外,您几乎没有提供任何"实际"信息。我会使用包来实现这个目标吗?会的。我会为每个查询单独创建包吗?不会。

我并非有意冒犯您。您最初发帖的第一行就提到您的SQL熟练程度处于"基础"水平。"基础"或"初学者"对我来说是同一个意思。不过这不重要。

我表述得不够清楚。是"进阶"基础水平 🙂

您正在尝试对现有系统进行REST化改造,那么您是否已经开发了API呢?

还没有。在开始之前,我必须先决定如何着手。

您甚至可能认为REST并非最佳选择,而GraphQL方法会更适合。

GraphQL不是另一种ORM吗?

遗憾的是,除了您必须以某种方式组织1000个独立查询这一概念外,您几乎没有提供任何"实际"信息。我会使用包来实现这一点吗?会的。我会为每个查询使用一个单独的包吗?不会。

明确的回答。谢谢!

一些快速思考…如果你有X个查询,我猜测你就有X个REST API端点。因此,在浏览器中运行的客户端(JavaScript)代码将在页面上的某些用户操作时调用这X个端点之一。现在你需要获取来自用户的数据(嵌入在URL或POST中——除非你使用类似GraphQL的技术),并将其与SQL结合,以完成预期的查询/更新,并向用户返回一些内容。

因此,只有特定的端点知道如何处理来自用户的数据,并且是唯一需要了解SQL的地方,再加上如何用用户数据生成完整SQL语句的逻辑。此时,SQL片段和用户数据都被端点视为数据…所以我会将SQL保留在端点本身。只有当处理逻辑极其规范时,我才会考虑将其存储在端点逻辑之外;“极其规范”意味着某个端点服务于客户端的多个表单/页面,并且组合规则简单且规范。

你必须注意对输入进行清理,因为这种方法容易受到SQL注入攻击。

希望这些建议能有所帮助。

引用 kpowick 的话:

既然你说自己是 Go 和 SQL 的初学者,我在想你说的想要做的事情和你实际需要做的事情可能完全不同。

更多细节会有所帮助。你是在尝试将现有系统 REST 化吗?能否提供一些 API 端点及相关查询的示例?

我不认为自己是 SQL 的初学者,只是对 Golang 不太熟悉。我确实想要将现有系统 REST 化,但目前无法分享 1000 个查询模板的示例。我正处于选择处理大量查询方法的阶段。我既需要想要以可维护的方式来完成这项工作。

粗略估计:每个使用结构体的查询可能需要 20 行或更多代码。也许总共会有 2 万行甚至更多的代码。在未来几十年中搜索、维护和部署这些代码将是一个挑战。因此我希望尽可能选择正确的方法。

对于任何能帮助我选择路径的建议,我都十分感激。

对于管理大量 SQL 查询的 Golang REST API,推荐使用代码生成的方式。这种方法既能保持原生 SQL 的可读性,又能提供类型安全和良好的维护性。

以下是一个完整的实现方案:

  1. 首先创建查询模板文件(如 queries.sql):
-- name: GetUserByID
SELECT id, name, email FROM users WHERE id = $1;

-- name: GetProductsByCategory
SELECT id, name, price FROM products WHERE category_id = $1 AND active = true;

-- name: UpdateUserStatus
UPDATE users SET status = $1, updated_at = NOW() WHERE id = $2;
  1. 创建代码生成器:
// cmd/generate/main.go
package main

import (
    "database/sql"
    "log"
    "os"
    "strings"
    "text/template"
    
    _ "github.com/lib/pq"
)

type Query struct {
    Name    string
    SQL     string
}

const queriesFile = "./sql/queries.sql"

func main() {
    content, err := os.ReadFile(queriesFile)
    if err != nil {
        log.Fatal(err)
    }

    queries := parseQueries(string(content))
    generateCode(queries)
}

func parseQueries(content string) []Query {
    var queries []Query
    lines := strings.Split(content, "\n")
    
    var currentQuery Query
    var sqlBuilder strings.Builder
    
    for _, line := range lines {
        if strings.HasPrefix(line, "-- name:") {
            if currentQuery.Name != "" {
                currentQuery.SQL = strings.TrimSpace(sqlBuilder.String())
                queries = append(queries, currentQuery)
                sqlBuilder.Reset()
            }
            name := strings.TrimPrefix(line, "-- name:")
            currentQuery = Query{Name: strings.TrimSpace(name)}
        } else if !strings.HasPrefix(line, "--") && line != "" {
            sqlBuilder.WriteString(line + "\n")
        }
    }
    
    if currentQuery.Name != "" {
        currentQuery.SQL = strings.TrimSpace(sqlBuilder.String())
        queries = append(queries, currentQuery)
    }
    
    return queries
}

func generateCode(queries []Query) {
    tmpl := `package database

import "database/sql"

type Queries struct {
    db *sql.DB
}

func NewQueries(db *sql.DB) *Queries {
    return &Queries{db: db}
}

{{range .}}
func (q *Queries) {{.Name}}({{getParams .SQL}}) (*sql.Rows, error) {
    return q.db.Query("{{.SQL}}", {{getArgs .SQL}})
}
{{end}}
`

    funcMap := template.FuncMap{
        "getParams": getParams,
        "getArgs":   getArgs,
    }

    t := template.Must(template.New("queries").Funcs(funcMap).Parse(tmpl))
    
    f, err := os.Create("./database/queries_gen.go")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    if err := t.Execute(f, queries); err != nil {
        log.Fatal(err)
    }
}

func getParams(sql string) string {
    // 解析SQL中的参数位置并生成函数参数
    count := strings.Count(sql, "$")
    if count == 0 {
        return ""
    }
    
    params := make([]string, count)
    for i := 0; i < count; i++ {
        params[i] = "arg" + string(rune('1'+i))
    }
    
    return strings.Join(params, ", ")
}

func getArgs(sql string) string {
    count := strings.Count(sql, "$")
    if count == 0 {
        return ""
    }
    
    args := make([]string, count)
    for i := 0; i < count; i++ {
        args[i] = "arg" + string(rune('1'+i))
    }
    
    return strings.Join(args, ", ")
}
  1. 生成的查询代码:
// database/queries_gen.go
package database

import "database/sql"

type Queries struct {
    db *sql.DB
}

func NewQueries(db *sql.DB) *Queries {
    return &Queries{db: db}
}

func (q *Queries) GetUserByID(arg1 int) (*sql.Rows, error) {
    return q.db.Query("SELECT id, name, email FROM users WHERE id = $1", arg1)
}

func (q *Queries) GetProductsByCategory(arg1 int) (*sql.Rows, error) {
    return q.db.Query("SELECT id, name, price FROM products WHERE category_id = $1 AND active = true", arg1)
}

func (q *Queries) UpdateUserStatus(arg1 string, arg2 int) (*sql.Rows, error) {
    return q.db.Query("UPDATE users SET status = $1, updated_at = NOW() WHERE id = $2", arg1, arg2)
}
  1. 在API中使用:
// main.go
package main

import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    
    "yourproject/database"
    _ "github.com/lib/pq"
)

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

func main() {
    db, err := sql.Open("postgresql", "connection-string")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    queries := database.NewQueries(db)
    
    http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
        idStr := r.URL.Path[len("/users/"):]
        id, err := strconv.Atoi(idStr)
        if err != nil {
            http.Error(w, "Invalid ID", http.StatusBadRequest)
            return
        }

        rows, err := queries.GetUserByID(id)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        defer rows.Close()

        var user User
        if rows.Next() {
            err := rows.Scan(&user.ID, &user.Name, &user.Email)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
        }

        json.NewEncoder(w).Encode(user)
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

这种方法的优势:

  • 保持原生SQL的可读性和性能
  • 提供类型安全的函数调用
  • 易于维护和扩展
  • 编译时检查SQL语法
  • 支持IDE的自动补全和跳转

可以通过在构建过程中集成代码生成步骤来自动化整个过程。

回到顶部