英文:
Correct approach to multiple queries Go Rest API
问题
我正在尝试使用Go创建一个REST API。目前部分功能已经实现,它可以返回4个单独的JSON对象,如下所示:
[{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps":""},
{"Name":"QA1","Server":"","Description":"","Apps":"Duo"},
{"Name":"QA1","Server":"","Description":"","Apps":"Git"},
{"Name":"QA1","Server":"","Description":"","Apps":"php"}]
我想要的是一个单一的返回对象,如下所示:
[{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps": "Duo|Git|php"}]
显然,我在查询或结构体(或两者或其他某些地方)的方式上存在问题。我想确保我理解如何正确实现,因为我希望在今后的查询和其他方面进行扩展。我在下面包含了完整的Go代码。
请注意,我不仅仅寻找解决方案(尽管我当然会感激与之进行比较),还希望知道我在思考中犯了什么错误以及正确的方法是什么。
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"log"
"net/http"
)
// There can be zero or more apps on a volume
type Apps struct {
Name string
}
// Volumes have a name, description, are on a server and have multiple services/apps
type Volume struct {
Name string
Server string
Description string
Services Apps
}
//Handle all requests
func Handler(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-type", "text/html")
webpage, err := ioutil.ReadFile("index.html")
if err != nil {
http.Error(response, fmt.Sprintf("home.html file error %v", err), 500)
}
fmt.Fprint(response, string(webpage))
}
// DB Connection
const (
DB_HOST = "mydbhost"
DB_NAME = "mydb"
DB_USER = "mydbuser"
DB_PASS = "mydbpass"
)
// Respond to URLs of the form /api
func APIHandler(response http.ResponseWriter, request *http.Request) {
//Connect to database
dsn := DB_USER + ":" + DB_PASS + "@" + DB_HOST + "/" + DB_NAME + "?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Println(err.Error())
}
defer db.Close()
// Open doesn't open a connection. Validate DSN data:
err = db.Ping()
if err != nil {
fmt.Println(err.Error())
}
//set mime type to JSON
response.Header().Set("Content-type", "application/json")
result := []*Volume{}
switch request.Method {
case "GET":
srvrnm := request.URL.Query().Get("srvrnm")
appnm := request.URL.Query().Get("appnm")
srvrs, err := db.Prepare("select VOLUMES.name as volnm, SERVERS.name as srvrnm, VOLUMES.description as descr From VOLUMES LEFT JOIN SERVERS ON VOLUMES.server_id = SERVERS.id where SERVERS.name = ?")
if err != nil {
fmt.Print(err)
}
srvcs, err := db.Prepare("select VOLUMES.name as volnm, SUPPRTSVCS.name as app_name From VOLUMES as VOLUMES JOIN HOSTSVCS ON VOLUMES.id = HOSTSVCS.volume_id JOIN SUPPRTSVCS ON SUPPRTSVCS.id = HOSTSVCS.supportsvcs_id where VOLUMES.name = ?")
if err != nil {
fmt.Print(err)
}
// Run the SQL Query to Get Volum & Description From Hostname
srvrrows, err := srvrs.Query(srvrnm)
if err != nil {
fmt.Print(err)
}
for srvrrows.Next() {
var volnm string
var srvrnm string
var descr string
// Scan the First Query
err = srvrrows.Scan(&volnm, &srvrnm, &descr)
if err != nil {
fmt.Println("Error scanning: " + err.Error())
return
}
// Append Slice with results from the scan
result = append(result, &Volume{Name: volnm, Server: srvrnm, Description: descr})
}
// Run the SQL Query for Services/Apps
srvcrows, err := srvcs.Query(appnm)
if err != nil {
fmt.Print(err)
}
for srvcrows.Next() {
var volnm string
var appnm string
// Scan the Second Query
err = srvcrows.Scan(&volnm, &appnm)
if err != nil {
fmt.Println("Error scanning: " + err.Error())
return
}
// Append Slice with results from the scan
result = append(result, &Volume{Name: volnm, Apps: appnm})
}
default:
}
json, err := json.Marshal(result)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintf(response, string(json))
db.Close()
}
func main() {
port := "1236"
var err string
mux := http.NewServeMux()
mux.Handle("/api", http.HandlerFunc(APIHandler))
mux.Handle("/", http.HandlerFunc(Handler))
// Start listing on a given port with these routes on this server.
log.Print("Listening on port " + port + " ... ")
errs := http.ListenAndServe(":"+port, mux)
if errs != nil {
log.Fatal("ListenAndServe error: ", err)
}
}
英文:
I am attempting to create a REST API in Go. I have it partially working in that it will return 4 separate json objects like such:
[{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps":""},
{"Name":"QA1","Server":"","Description":"","Apps":"Duo"},
{"Name":"QA1","Server":"","Description":"","Apps":"Git"},
{"Name":"QA1","Server":"","Description":"","Apps":"php"}]
What I want is a single returned object like:
[{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps": "Duo|Git|php"}]
I obviously have the way that I am either making my queries or the structs (or both or something else) not quite correct. I want to make sure I understand how to do this right because I would like to expand on it for other queries and such down the road. I have included the "full" go code below.
To be clear, I'm not simply looking for the solution (though I would of course appreciate that to compare with), but where I've gone wrong in my thinking and what the correct approach would be.
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"log"
"net/http"
)
// There can be zero or more apps on a volume
type Apps struct {
Name string
}
// Volumes have a name, description, are on a server and have multiple services/apps
type Volume struct {
Name string
Server string
Description string
Services Apps
}
//Handle all requests
func Handler(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-type", "text/html")
webpage, err := ioutil.ReadFile("index.html")
if err != nil {
http.Error(response, fmt.Sprintf("home.html file error %v", err), 500)
}
fmt.Fprint(response, string(webpage))
}
// DB Connection
const (
DB_HOST = "mydbhost"
DB_NAME = "mydb"
DB_USER = "mydbuser"
DB_PASS = "mydbpass"
)
// Respond to URLs of the form /api
func APIHandler(response http.ResponseWriter, request *http.Request) {
//Connect to database
dsn := DB_USER + ":" + DB_PASS + "@" + DB_HOST + "/" + DB_NAME + "?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Println(err.Error())
}
defer db.Close()
// Open doesn't open a connection. Validate DSN data:
err = db.Ping()
if err != nil {
fmt.Println(err.Error())
}
//set mime type to JSON
response.Header().Set("Content-type", "application/json")
result := []*Volume{}
switch request.Method {
case "GET":
srvrnm := request.URL.Query().Get("srvrnm")
appnm := request.URL.Query().Get("appnm")
srvrs, err := db.Prepare("select VOLUMES.name as volnm, SERVERS.name as srvrnm, VOLUMES.description as descr From VOLUMES LEFT JOIN SERVERS ON VOLUMES.server_id = SERVERS.id where SERVERS.name = ?")
if err != nil {
fmt.Print(err)
}
srvcs, err := db.Prepare("select VOLUMES.name as volnm, SUPPRTSVCS.name as app_name From VOLUMES as VOLUMES JOIN HOSTSVCS ON VOLUMES.id = HOSTSVCS.volume_id JOIN SUPPRTSVCS ON SUPPRTSVCS.id = HOSTSVCS.supportsvcs_id where VOLUMES.name = ?")
if err != nil {
fmt.Print(err)
}
// Run the SQL Query to Get Volum & Description From Hostname
srvrrows, err := srvrs.Query(srvrnm)
if err != nil {
fmt.Print(err)
}
for srvrrows.Next() {
var volnm string
var srvrnm string
var descr string
// Scan the First Query
err = srvrrows.Scan(&volnm, &srvrnm, &descr)
if err != nil {
fmt.Println("Error scanning: " + err.Error())
return
}
// Append Slice with results from the scan
result = append(result, &Volume{Name: volnm, Server: srvrnm, Description: descr})
}
// Run the SQL Query for Services/Apps
srvcrows, err := srvcs.Query(appnm)
if err != nil {
fmt.Print(err)
}
for srvcrows.Next() {
var volnm string
var appnm string
// Scan the Second Query
err = srvcrows.Scan(&volnm, &appnm)
if err != nil {
fmt.Println("Error scanning: " + err.Error())
return
}
// Append Slice with results from the scan
result = append(result, &Volume{Name: volnm, Apps: appnm})
}
default:
}
json, err := json.Marshal(result)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintf(response, string(json))
db.Close()
}
func main() {
port := "1236"
var err string
mux := http.NewServeMux()
mux.Handle("/api", http.HandlerFunc(APIHandler))
mux.Handle("/", http.HandlerFunc(Handler))
// Start listing on a given port with these routes on this server.
log.Print("Listening on port " + port + " ... ")
errs := http.ListenAndServe(":"+port, mux)
if errs != nil {
log.Fatal("ListenAndServe error: ", err)
}
}
答案1
得分: 2
根据你的描述,你希望结果如下所示:
[
{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps": ["Duo","Git","php"]}
]
因此,你希望你的Volumes
结构体如下所示:
type Volume struct {
Name string
Server string
Description string
Services []Apps
}
如果你希望Apps
实际输出为Duo|Git|php
,你可以创建一个自定义类型,而不是使用[]Apps
,并实现一个JSON Marshaler。这个实现可以简单地返回json.Marshal(strings.join(names,"|"))
与其运行两个单独的查询,更高效的做法是运行一个选择卷和应用程序的联合查询。重要的是,这个查询按卷排序,以便所有卷行是连续的。示例查询输出如下所示:
Name | Server | Desc | App
---- | ------ | ----- | ---
Vol1 | Srv1 | Desc1 | App1
Vol1 | Srv1 | Desc1 | App2
Vol2 | Srv2 | Desc2 | App3
然后,你可以循环遍历这个结果,并检测是否正在查看一个新的卷。如果是,就在结果中创建一个新条目。如果不是,则将应用程序添加到应用程序列表中。例如:
var (
volnm string
srvrnm string
descr string
appnm string
v *Volume
result []*Volume
)
for srvrrows.Next() {
if err = srvcrows.Scan(&volnm, &srvrnm, &descr, &appnm); err != nil {
// 处理错误
}
// 如果是同一个卷,将应用程序添加到当前卷中,否则开始一个新的卷
if v != nil && v.Name == volnm {
v.Services = append(v.Services, Apps{appnm})
} else {
v = &Volume{
Name: volnm,
Server: srvrnm,
Description: descr,
Services: []Apps{appnm}}
result = append(result, v)
}
}
// 完成,返回结果等...
在采用这种方法时,你需要一个适当的父记录鉴别器。我只是为了说明目的而使用了v.Name == volnm
,但实际上应该检查主键。如果你不希望通过API导出它,可以将其设置为未导出的(小写)字段。
英文:
From the sounds of it, you want to your result to look like:
[
{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps": ["Duo","Git","php"]
]
Hence you want your Volumes struct to look like:
type Volume struct {
Name string
Server string
Description string
Services []Apps
}
If you want the Apps to actually output Duo|Git|php
then you could create a custom type instead of []Apps
with a JSON Marshaler implementation. This could simply return json.Marshal(strings.join(names,"|"))
Rather than run two separate queries, it would be more efficient to run a single query that selects the product of volumes & apps together. It is important that this query is sorted by volume so all volume rows are contiguous. Example query output would be:
Name | Server | Desc | App
---- | ------ | ----- | ---
Vol1 | Srv1 | Desc1 | App1
Vol1 | Srv1 | Desc1 | App2
Vol2 | Srv2 | Desc2 | App3
You would then loop over this and detect if you are looking at a new volume. If so, create a new entry in the result. If not, add the App to the list of apps. For example:
var (
volnm string
srvrnm string
descr string
appnm string
v *Volume
result []*Volume
)
for srvrrows.Next() {
if err = srvcrows.Scan(&volnm, &srvrnm, &descr, &appnm);err!=nil {
// Handle error
}
// Add App to current volume if same, otherwise start a new volume
if v!=nil && v.Name == volnm {
v.Services = append(v.Services,Apps{appnm})
} else {
v = &Volume{
Name: volnm,
Server: svrnm,
Description: descr,
Services: []Apps{appnm}}
result = append(result,v)
}
}
// Finished, return result etc...
When taking this approach, you need an appropriate parent record discriminator. I'd just used v.Name == volnm
for illustration purposes but this should really be checking the primary key. You can make this an unexported (lowercase) field in the struct if you do not wish to export it through the API.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论