使用Golang來寫一個RESTful API - Part2
我的程式架構如下圖所示,Client可以通過api來讀寫資料庫中的資料。這篇文章主要是討論backend的部分要如何實現。本實作會使用Gin和Gorm來完成,Database則使用MariaDB。
這篇文章是延續上一篇的內容,多完成了對資料庫進行Create, Update, Delete以及Join這種比較複雜的查詢。完成的程式碼可以參考我的github。
架構圖
資料庫Scheme
資料來源:https://www.mariadbtutorial.com/getting-started/mariadb-sample-database/
完成的成果有以下功能:
GET /api/GetLanguage/:id --> 根據id讀出對應的Language
GET /api/GetLanguageRange/:start/:end --> 根據給定id範圍,讀出所有Language
GET /api/GetCountryUselanguage/?counry --> 根據query的country,讀出其所使用語言
POST /api/AddLanguage --> 根據POST的Body(json)來新增資料
DELETE /api/DeleteLanguage/:language --> 根據language刪除該筆資料
PUT /api/UpdataLanguage --> 根據PUT的Body(json)更新資料
開發環境
- Golang
- MariaDB
Database的部分採用MariaDB,資料使用的是網路上的sample database可以直接下載。 建議可以使用docker-compose來架設,這裡有寫好的docker-compose設定(mariadb-phpmyadmin),phpmyadmin也這串好了。
使用方式:
可以自己修改image, username, password,最後cd
到mariadb-phpmyadmin
資料夾下docker-compose up -d
就可以啟動一個MariaDB了。
- API測試工具
- Thunder Client: 是一個VSCode的Extension,可以用來取代Postman而且還不用額外安裝其它程式
程式碼架構
程式進入點為main.go,資料庫連線設定在database
資料夾,而我會將整個API拆成三個部分,分別放在不同的資料夾:
routes
: 設定api的路徑,與對應的functioncontrollers
: 處理api收到request(route對應到的function)services
: 對資料庫操作
.
|-- routes
| |-- api-routes.go
| `-- route-utils.go
|-- controllers
| |-- language-controller.go
| `-- hostInfo-controller.go
|-- services
| `-- Language-service.go
|-- database
| |-- scheme.go
| `-- database.go
|-- go.mod
|-- go.sum
`-- main.go
實作程式
將資料庫的四種操作分別對應到HTTP的四種方法。而我所設計的API提供這四種功能來對資料庫進行操作
資料庫功能 | HTTP Method |
---|---|
查詢Read (SELECT) | GET |
新增Create (INSERT) | POST |
更新Update (UPDATE) | PUT |
刪除Delete (DELETE) | DELETE |
開始實作
查詢Read
查詢可以分為簡單的查詢
SELECT * FROM `language` WHERE id=1
或是JOIN這種多個Table的複雜查詢
SELECT C.name AS Country_Name, L.language FROM `countries` AS C
JOIN `country_languages` AS CL
ON CL.country_id = C.country_id
JOIN `languages` AS L
ON CL.language_id = L.language_id
簡單的查詢
我們可以使用Gorm來對資料庫下指令,database.DB
是我們的程式和databse建立的連線。
以這個查詢為例,這個例子應該蠻好理解的Where()
是條件,Find(&languages)
會把查詢結果放進去languages
變數之中。
SQL
SELECT * FROM `language` WHERE `Language_id` BETWEEN 10 AND 20
Code
func ReadLanguages(startId string, endId string) ([]database.Languages, error) { var languages []database.Languages // SELECT * FROM `language` WHERE `Language_id` BETWEEN 10 AND 20 result := database.DB.Where("Language_id BETWEEN ? AND ?", startId, endId).Find(&languages) return languages, result.Error }
複雜查詢
以這個查詢為例,有三個table做JOIN,分別是countries
、languages
和country_languages
,由Joins()
指定JOIN的條件,而Where()
是SELECT…FROM…WHERE的條件,最後Scan(&r)
把結果存入r
這個變數之中。
提醒一點,structure的欄位要和查詢的結果column名稱相同,也就是要根據C.name AS CountryName
的部分~
SQL
SELECT C.name AS Country_Name, L.language FROM `countries` AS C JOIN `country_languages` AS CL ON CL.country_id = C.country_id JOIN `languages` AS L ON CL.language_id = L.language_id
Code
func GetCountryUsedLanguages(country string) ([]database.CountryUesdLanguage, error) { var r []database.CountryUesdLanguage result := database.DB. Table("countries AS C"). Select("C.name AS CountryName, L.language AS Language"). Joins("JOIN country_languages AS CL ON CL.country_id = C.country_id"). Joins("JOIN languages AS L ON CL.language_id = L.language_id"). Where("C.name = ?", country). Scan(&r) return r, result.Error }
type CountryUesdLanguage struct { CountryName string Language string }
新增Create
InsertLanguage()
的部分,Gorm會根據structure的名稱去選擇Table
SQL
INSERT INTO `Language` (`Language_id`, `Language`) VALUES (500, "Test1"), (500, "Test1")
code
func InsertLanguage(items []database.Languages) (int64, error) { // INSERT INTO `Language` (`Language_id`, `Language`) VALUES (500, "Test1"), (500, "Test1") result := database.DB.Create(items) return result.RowsAffected, result.Error }
刪除Delete
DeleteLanguage()
的部分,Delete(&database.Languages{})
DELETE並不需要回傳資料,其用意應該跟上面(新增Create)的意思差不多,Gorm會根據structure的名稱去選擇Table,
SQL
DELETE FROM `Language` WHERE `Language` = "Test1"
code
func DeleteLanguage(languageName string) (int64, error) { // DELETE FROM `Language` WHERE `Language` = "Test1" result := database.DB.Where("Language = ?", languageName).Delete(&database.Languages{}) return result.RowsAffected, result.Error }
更新Update
SQL
UPDATE `Language` SET `Language` = "UpdatedLanguage" WHERE `Language_id` = 500
code
func UpdateLanguage(item database.Languages) (int64, error) { // UPDATE `Language` SET `Language` = "UpdatedLanguage" WHERE `Language_id` = 500 result := database.DB.Model(&database.Languages{}).Where("Language_id = ?", item.Language_id).Update("Language", item.Language) return result.RowsAffected, result.Error }
處理Request
GET api/GetLanguageRange/:start/:end
範例:查詢Language_id為10~20的資料
http://localhost:3000/api/GetLanguageRange/10/20
當有Request戳到GET api/GetLanguageRange/:start/:end
時,會執行GetLanguages()
這個HandleFunc()。
func setupLanguageRoute() {
register("GET", "/api/GetLanguage/:id", controllers.GetLanguage)
register("GET", "/api/GetLanguageRange/:start/:end", controllers.GetLanguages)
register("GET", "/api/GetCountryUselanguage", controllers.GetCountryUesdLanguages)
}
由c.Param()
來取出參數,帶入用Gorm寫好的function來取得SQL的查詢結果
func GetLanguages(c *gin.Context) {
// get URL parameters
startId := c.Param("start")
endId := c.Param("end")
// Call function to get data from database
languages, err := services.ReadLanguages(startId, endId)
if err != nil {
c.JSON(200, gin.H{"message": err.Error()})
} else {
// Response result to Client
c.JSON(200, gin.H{"message": "success", "result": languages})
}
}
回傳果如下
{
"message": "success",
"result": [
{
"Language_id": 10,
"Language": "Ambo"
},
{
"Language_id": 11,
"Language": "Chokwe"
},
{
"Language_id": 12,
"Language": "Kongo"
},
{
"Language_id": 13,
"Language": "Luchazi"
}
]
}
GET /api/GetCountryUselanguage/?counry
範例:查詢Canada所使用的Language為何?
http://localhost:3000/api/GetCountryUselanguage/?country=Canada
當有Request戳到GET api/GetCountryUselanguage/?counry
時,會執行GetCountryUesdLanguages()
這個HandleFunc()
和前一個差異在於?country=Canada
這個稱為一個query,可以視為一組key, value,可以使用c.Query("country")
取出值
func GetCountryUesdLanguages(c *gin.Context) {
country := c.Query("country")
var result []database.CountryUesdLanguage
result, err := services.GetCountryUsedLanguages(country)
if err != nil {
c.JSON(200, gin.H{"message": err.Error()})
} else {
// Response data to Client
c.JSON(200, gin.H{"message": "success", "result": result})
}
}
回傳果如下
{
"message": "success",
"result": [
{
"CountryName": "Canada",
"Language": "Dutch"
},
{
"CountryName": "Canada",
"Language": "English"
},
...
{
"CountryName": "Canada",
"Language": "Chinese"
},
{
"CountryName": "Canada",
"Language": "Eskimo Languages"
},
{
"CountryName": "Canada",
"Language": "Punjabi"
}
]
}
POST /api/AddLanguage
範例:新增n筆Language資料
http://localhost:3000/api/AddLanguage
POST帶的Body如下,為一個json含有多筆要新增的資料
[
{
"Language_id": 500,
"Language": "Test500"
},
{
"Language_id": 501,
"Language": "Test501"
}
]
當有Request戳到POST /api/AddLanguage
時,會執行AddLanguage()
這個HandleFunc()
這裡使用Bind(&m)
將body內的json給parse到structure之中
func AddLanguage(c *gin.Context) {
var m []database.Languages
c.Bind(&m)
rowsAffected, err := services.InsertLanguage(m)
if err != nil {
c.JSON(200, gin.H{"message": err.Error()})
} else {
c.JSON(200, gin.H{"message": "success", "rowsAffected": rowsAffected})
}
}
DELETE /api/DeleteLanguage/:language
範例:刪除所有Language=Test500的資料
http://localhost:3000/api/RemoveLanguage/Test501
當有Request戳到DELETE /api/DeleteLanguage/:language
時,會執行RemoveLanguage()
這個HandleFunc()
這裡與前面查詢取得URL參數的方法相同,一樣使用c.Param()
func RemoveLanguage(c *gin.Context) {
queryId := c.Param("language")
rowsAffected, err := services.DeleteLanguage(queryId)
if err != nil {
c.JSON(200, gin.H{"message": err.Error()})
} else {
var mesg string
if rowsAffected == 0 {
mesg = "Language not found"
} else {
mesg = "success"
}
c.JSON(200, gin.H{"message": mesg, "rowsAffected": rowsAffected})
}
}
PUT /api/UpdataLanguage
範例:更新Language_id=xxx的資料
http://localhost:3000/api/UpdateLanguage
POST帶的Body如下,為一個json表示要將Language_id=500的資料更新成"Updated500"
{
"Language_id": 500,
"Language": "Updated500"
}
當有Request戳到PUT /api/UpdataLanguage
時,會執行UpdateLanguage()
這個HandleFunc()
這裡與前面新增資料時的方法相同,一樣使用c.Bind(&m)
把json給parse到structure之中
func UpdateLanguage(c *gin.Context) {
var m database.Languages
c.Bind(&m)
rowsAffected, err := services.UpdateLanguage(m)
if err != nil {
c.JSON(200, gin.H{"message": err.Error()})
} else {
c.JSON(200, gin.H{"message": "success", "rowsAffected": rowsAffected})
}
}
以上就是用Gin來開發一個的API的簡單範例~
下一篇文章考慮記錄一下我使用Github Action自動化,將這個API部署到Azure k8s的方式。
敬請期待~~