【Gin-API系列】配置檔案和資料庫操作(三)

RunningPower發表於2020-08-10

我們前面已經實現了API的基礎版本,能對引數校驗和返回指定資料,這一章,我們將對主機和交換機進行建模,存入資料庫。
考慮到資料庫安裝和使用的簡便性,我們採用文件儲存結構的MongoDB資料庫。

Mongo資料庫下載安裝,安裝後不用設定密碼,直接使用即可
下載連結 https://www.filehorse.com/download-mongodb/download/ 或者 https://www.mongodb.com/try/download/community

配置檔案

使用資料庫之前需要配置資料庫地址和埠,所以我們將配置資訊存放到配置檔案,採用yaml格式儲存解析

  • 配置檔案內容

這裡,我們還將API監聽的地址和埠,日誌路徑也可以都配上

api_server:
  env: prod
  host: 127.0.0.1
  port: 9000
mgo:
  uri: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
  database: gin_ips
  pool_size: 100
log:
  path: log/gin_ips
  level: DEBUG
  name: gin.log
  count: 180
  • Golang Yaml檔案解析
// 通用 Config 介面
type Config interface {
	InitError(msg string) error
}

// 根據yaml檔案初始化通用配置, 無需輸出日誌
func InitYaml(filename string, config Config) error {
	fp, err := os.Open(filename)
	if err != nil {
		msg := fmt.Sprintf("configure file [ %s ] not found", filename)
		return config.InitError(msg)
	}
	defer func() {
		_ = fp.Close()
	}()
	if err := yaml.NewDecoder(fp).Decode(config); err != nil {
		msg := fmt.Sprintf("configure file [ %s ] initialed failed", filename)
		return config.InitError(msg)
	}
	return nil
}

Golang 使用Mongo資料庫

golang中有多種優秀的orm庫,例如xorm,gorm。由於orm將資料庫模型和語言緊密封裝,使用起來非常方便,很適合web前端開發。
但與此同時,使用orm也會導致部分效能丟失(深度使用後會發現隱藏的坑也不少),有興趣的同學可以瞭解下。
本文主要使用golang官方mongodb庫mongo-driver,直接通過sql語句運算元據庫(這個過程可以學習下如何explain和優化sql語句)。

  • 模型定義
type Collection struct {
	client     *mongo.Collection
	database   string // 資料庫
	collection string // 集合
}
  • 建立連線池
// 連線池建立
func CreatePool(uri string, size uint64) (pool *mongo.Client, e error) {
	defer func() {
		if err := recover(); err != nil {
			e = errors.New(fmt.Sprintf("%v", err))
		}
	}()
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) // 10s超時
	defer cancel()

	var err error
	pool, err = mongo.Connect(ctx, options.Client().ApplyURI(uri).SetMinPoolSize(size)) // 連線池
	if err != nil {
		return pool, err
	}
	err = pool.Ping(context.Background(), nil) // 檢查連線
	if err != nil {
		return pool, err
	}
	return pool, nil
}
  • 銷燬連線池
func DestroyPool(client *mongo.Client) error {
	err := client.Disconnect(context.Background())
	if err != nil {
		return err
	}
	return nil
}
  • 查詢操作
// 查詢單個文件, sort 等於1表示 返回最舊的,sort 等於-1 表示返回最新的
/*
BSON(二進位制編碼的JSON) D家族  bson.D
D:一個BSON文件。這種型別應該在順序重要的情況下使用,比如MongoDB命令。
M:一張無序的map。它和D是一樣的,只是它不保持順序。
A:一個BSON陣列。
E:D裡面的一個元素。
*/
func (m *Collection) FindOne(filter bson.D, sort, projection bson.M) (bson.M, error) {
	findOptions := options.FindOne().SetProjection(projection)
	if sort != nil {
		findOptions = findOptions.SetSort(sort)
	}
	singleResult := m.client.FindOne(context.Background(), filter, findOptions)
	var result bson.M
	if err := singleResult.Decode(&result); err != nil {
		return result, err
	}
	return result, nil
}

/*
查詢多個 sort 等於1表示 返回最舊的,sort 等於-1 表示返回最新的
每次只返回1頁 page size 大小的資料
project 不能混合 True 和 False
*/
func (m *Collection) FindLimit(filter bson.D, page, pageSize uint64, sort, projection bson.M) ([]bson.M, error) {
	var resultArray []bson.M
	if page == 0 || pageSize == 0 {
		return resultArray, errors.New("page or page size can't be 0")
	}
	skip := int64((page - 1) * pageSize)
	limit := int64(pageSize)
	if projection == nil {
		projection = bson.M{}
	}
	findOptions := options.Find().SetProjection(projection).SetSkip(skip).SetLimit(limit)
	if sort != nil {
		findOptions = findOptions.SetSort(sort)
	}
	cur, err := m.client.Find(context.Background(), filter, findOptions)
	if err != nil {
		return resultArray, err
	}
	defer func() {
		_ = cur.Close(context.Background())
	}()
	for cur.Next(context.Background()) {
		var result bson.M
		err := cur.Decode(&result)
		if err != nil {
			return resultArray, err
		}
		resultArray = append(resultArray, result)
	}
	//err = cur.All(context.Background(), &resultArray)
	if err := cur.Err(); err != nil {
		return resultArray, err
	}
	return resultArray, nil
}

// 返回查詢條件的全部文件記錄
// project 不能混合 True 和 False
func (m *Collection) FindAll(filter bson.D, sort, projection bson.M) ([]bson.M, error) {
	var resultArray []bson.M
	if projection == nil {
		projection = bson.M{}
	}
	findOptions := options.Find().SetProjection(projection)
	if sort != nil {
		findOptions = findOptions.SetSort(sort)
	}
	cur, err := m.client.Find(context.Background(), filter, findOptions)
	if err != nil {
		return resultArray, err
	}
	defer func() {
		_ = cur.Close(context.Background())
	}()
	for cur.Next(context.Background()) {
		// fmt.Println(cur.Current)
		var result bson.M
		err := cur.Decode(&result)
		if err != nil {
			return resultArray, err
		}
		resultArray = append(resultArray, result)
	}
	if err := cur.Err(); err != nil {
		return resultArray, err
	}
	return resultArray, nil
}

  • 新增操作
//插入單個
func (m *Collection) InsertOne(document interface{}) (primitive.ObjectID, error) {
	insertResult, err := m.client.InsertOne(context.Background(), document)
	var objectId primitive.ObjectID
	if err != nil {
		return objectId, err
	}
	objectId = insertResult.InsertedID.(primitive.ObjectID)
	return objectId, nil
}

//插入多個文件
func (m *Collection) InsertMany(documents []interface{}) ([]primitive.ObjectID, error) {
	var insertDocs []interface{}
	for _, doc := range documents {
		insertDocs = append(insertDocs, doc)
	}
	insertResult, err := m.client.InsertMany(context.Background(), insertDocs)
	var objectIds []primitive.ObjectID
	if err != nil {
		return objectIds, err
	}
	for _, oid := range insertResult.InsertedIDs {
		objectIds = append(objectIds, oid.(primitive.ObjectID))
	}
	return objectIds, nil
}
  • 修改操作
/*
更新 filter 返回的第一條記錄
如果匹配到了(matchCount >= 1) 並且 objectId.IsZero()= false 則是新插入, objectId.IsZero()=true則是更新(更新沒有獲取到id)
ObjectID("000000000000000000000000")
document 修改為 interface 表示支援多個欄位更新,使用bson.D ,即 []bson.E
*/
func (m *Collection) UpdateOne(filter bson.D, document interface{}, insert bool) (int64, primitive.ObjectID, error) {
	updateOption := options.Update().SetUpsert(insert)
	updateResult, err := m.client.UpdateOne(context.Background(), filter, document, updateOption)
	var objectId primitive.ObjectID
	if err != nil {
		return 0, objectId, err
	}
	if updateResult.UpsertedID != nil {
		objectId = updateResult.UpsertedID.(primitive.ObjectID)
	}
	// fmt.Println(objectId.IsZero())
	return updateResult.MatchedCount, objectId, nil
}

/*
更新 filter 返回的所有記錄,返回的匹配是指本次查詢匹配到的所有數量,也就是最後更新後等於新的值的數量
如果匹配到了(matchCount >= 1) 並且 objectId.IsZero()= false 則是新插入, objectId.IsZero()=true則是更新(更新沒有獲取到id)
ObjectID("000000000000000000000000")
docAction 修改為 interface 表示支援多個欄位更新,使用bson.D ,即 []bson.E
*/
func (m *Collection) UpdateMany(filter bson.D, docAction interface{}, insert bool) (int64, primitive.ObjectID, error) {
	updateOption := options.Update().SetUpsert(insert)
	updateResult, err := m.client.UpdateMany(context.Background(), filter, docAction, updateOption)
	var objectId primitive.ObjectID
	if err != nil {
		return 0, objectId, err
	}
	if updateResult.UpsertedID != nil {
		objectId = updateResult.UpsertedID.(primitive.ObjectID)
	}
	// fmt.Println(objectId.IsZero())
	return updateResult.MatchedCount, objectId, nil
}
/*
替換 filter 返回的1條記錄(最舊的)
如果匹配到了(matchCount >= 1) 並且 objectId.IsZero()= false 則是新插入, objectId.IsZero()=true則是更新(更新沒有獲取到id)
ObjectID("000000000000000000000000")
採用 FindOneAndReplace 在查詢不到但正確插入新的資料會有"mongo: no documents in result" 的錯誤
*/
func (m *Collection) Replace(filter bson.D, document interface{}, insert bool) (int64, primitive.ObjectID, error) {
	option := options.Replace().SetUpsert(insert)
	replaceResult, err := m.client.ReplaceOne(context.Background(), filter, document, option)
	var objectId primitive.ObjectID
	if err != nil {
		return 0, objectId, err
	}
	if replaceResult.UpsertedID != nil {
		objectId = replaceResult.UpsertedID.(primitive.ObjectID)
	}
	// fmt.Println(objectId.IsZero())
	return replaceResult.MatchedCount, objectId, nil
}

  • 刪除操作
/*
查詢並刪除一個  sort 等於1表示 刪除最舊的,sort 等於-1 表示刪除最新的
一般根據 id 查詢就會保證刪除正確
*/
func (m *Collection) DeleteOne(filter bson.D, sort bson.M) (bson.M, error) {
	findOptions := options.FindOneAndDelete()
	if sort != nil {
		findOptions = findOptions.SetSort(sort)
	}
	singleResult := m.client.FindOneAndDelete(context.Background(), filter, findOptions)
	var result bson.M
	if err := singleResult.Decode(&result); err != nil {
		return result, err
	}
	return result, nil
}

/*
根據條件刪除全部
*/
func (m *Collection) DeleteAll(filter bson.D) (int64, error) {
	count, err := m.client.DeleteMany(context.Background(), filter)
	if err != nil {
		return 0, err
	}
	return count.DeletedCount, nil
}

  • 建立索引
// 建立索引,重複建立不會報錯
func (m *Collection) CreateIndex(index string, unique bool) (string, error) {
	indexModel := mongo.IndexModel{Keys: bson.M{index: 1}, Options: options.Index().SetUnique(unique)}
	name, err := m.client.Indexes().CreateOne(context.Background(), indexModel)
	return name, err
}

資料模型設計和返回

  • 模型設計

根據需求,我們將主機和交換機的各個欄位提前定義好,這時候可以考慮各個模型分開儲存到多個集合,也可以合併成1個(本文選擇合併)

//|ID|主機名|IP|記憶體大小|磁碟大小|型別|負責人|

type HostModel struct {
	Oid      configure.Oid   `json:"oid"` // 考慮所有例項存放在同一個集合中,需要一個欄位來區分
	Id       string   `json:"id"`
	Ip       string   `json:"ip"`
	Hostname string   `json:"hostname"`
	MemSize  int64    `json:"mem_size"`
	DiskSize int64    `json:"disk_size"`
	Class    string   `json:"class"` // 主機型別
	Owner    []string `json:"owner"`
}

//|ID|裝置名|管理IP|虛IP|帶外IP|廠家|負責人|

type SwitchModel struct {
	Oid           configure.Oid   `json:"oid"` // 考慮所有例項存放在同一個集合中,需要一個欄位來區分
	Id            string   `json:"id"`
	Name          string   `json:"name"`
	Ip            string   `json:"ip"`
	Vip           []string `json:"vip"`
	ConsoleIp     string   `json:"console_ip"`
	Manufacturers string   `json:"manufacturers"` // 廠家
	Owner         []string `json:"owner"`
}
  • 手工錄入資料

隨機插入幾條測試資料即可

hmArr := []HostModel{
		{
			Oid:      configure.OidHost,
			Id:       "H001",
			Ip:       "10.1.162.18",
			Hostname: "10-1-162-18",
			MemSize:  1024000,
			DiskSize: 102400000000,
			Class:    "物理機",
			Owner:    []string{"小林"},
		},
		{
			Oid:      configure.OidHost,
			Id:       "H002",
			Ip:       "10.1.162.19",
			Hostname: "10-1-162-19",
			MemSize:  1024000,
			DiskSize: 102400000000,
			Class:    "虛擬機器",
			Owner:    []string{"小黃"},
		},
	}
  • API呼叫返回

最後我們修改下引數的驗證規則,支援返回指定模型和返回所有模型。測試結果如下:

curl "http://127.0.0.1:8080?ip=10.1.162.18"

{"code":0,"message":"","data":{"page":1,"page_size":2,"size":2,"total":2,"list":[{"class":"物理機","disksize":102400000000,"hostname":"10-1-162-18","id":"H001","ip":"10.1.162.18","
memsize":1024000,"oid":"HOST","owner":["小林"]},{"consoleip":"10.3.32.11","id":"S001","ip":"10.2.32.11","manufacturers":"華為","name":"上海叢集交換機","oid":"SWITCH","owner":["老馬
","老曹"],"vip":["10.2.20.1","10.2.20.13","10.1.162.18"]}]}}

curl "http://127.0.0.1:8080?ip=10.1.162.18&oid=HOST"

{"code":0,"message":"","data":{"page":1,"page_size":1,"size":1,"total":1,"list":[{"class":"物理機","disksize":102400000000,"hostname":"10-1-162-18","id":"H001","ip":"10.1.162.18","
memsize":1024000,"oid":"HOST","owner":["小林"]}]}}

本文模擬生產環境完成了資料庫的設計和資料配置,下一章,我們將配置Gin Log,同時開始使用Gin中介軟體完善API。

Github 程式碼

請訪問 Gin-IPs 或者搜尋 Gin-IPs

相關文章