多个查询的正确方法 Go Rest API

huangapple go评论159阅读模式
英文:

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.

huangapple
  • 本文由 发表于 2017年7月28日 21:21:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/45374658.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定