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"级别。但我需要一种能够处理数百个结构体和查询的方法。要易于编码、易于理解和维护。到目前为止,我找到了三种实现方式:
- 使用模板或类似方式存储查询。https://github.com/gchaincl/dotsql
- 使用 1000 个包。但这对我来说似乎难以管理。
- 将查询与结构体一起存储在查找用的 PostgreSQL 数据库中。获取所需查询并执行查询。这样易于维护,但会增加可能影响速度的额外层。
除了这些想法,我还尝试过动态方法但没有成功。
我想知道是否有人遇到过这种情况并能分享一些想法和建议?
先谢谢了!
更多关于Golang实现支持1000种查询模板的REST API开发指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html
我是否应该把你的回答理解为:“使用1000个包”?
更多关于Golang实现支持1000种查询模板的REST API开发指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
是的,它有效! 到目前为止我们还没有检测到延迟。
是的,我做到了!
这涉及到大约四百个SQL调用公式。由于公式可能随时更改,因此利用这种数据库方法非常有用。
编程愉快!
你的 SQL 可能包含一些重复部分,建议使用 SQL 构建器包(例如 go-sqlbuilder)并在可能的情况下重用代码。
Marco_Azevedo:
是的,我做到了!
大约有四百个SQL调用公式。这些公式可能随时会更改,因此利用这种数据库方法非常有用。
很高兴听到我的想法在现实中可行。没有因为数据库造成任何延迟吗?
这样来看,你有多大可能会修改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 的可读性,又能提供类型安全和良好的维护性。
以下是一个完整的实现方案:
- 首先创建查询模板文件(如
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;
- 创建代码生成器:
// 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, ", ")
}
- 生成的查询代码:
// 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)
}
- 在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的自动补全和跳转
可以通过在构建过程中集成代码生成步骤来自动化整个过程。


