我的程式架構如下圖所示,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
    • gorm: ORM library for Golang
    • gin: HTTP web framework
  • MariaDB
    • Database的部分採用MariaDB,資料使用的是網路上的sample database可以直接下載。 建議可以使用docker-compose來架設,這裡有寫好的docker-compose設定(mariadb-phpmyadmin),phpmyadmin也這串好了。

      使用方式:
      可以自己修改image, username, password,最後cdmariadb-phpmyadmin資料夾下docker-compose up -d就可以啟動一個MariaDB了。

  • API測試工具
    • Thunder Client: 是一個VSCode的Extension,可以用來取代Postman而且還不用額外安裝其它程式

程式碼架構

程式進入點為main.go,資料庫連線設定在database資料夾,而我會將整個API拆成三個部分,分別放在不同的資料夾:

  • routes: 設定api的路徑,與對應的function
  • controllers: 處理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,分別是countrieslanguagescountry_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的方式。

敬請期待~~