介紹
本文章我們來學習一下使用Go對MongoDB資料庫的實戰,這裡我們將以微信小程式中的三個微服務為背景,分別來實現對這三個微服務的CRUD實戰,來體驗Go和MongoDB在實際開發中的魅力
環境配置
開發環境:VScode
golang:go語言官方
MongoDB:使用driver@v1.8.4/mongo">MongoDB官方包
微信小程式介紹
該小程式是一款租車小程式dome,使用GRPC框架引領全棧開發,前端:typescrtp+wxml+wxss,後端:Go,GRPC框架,資料庫:MongoDB
還在學習中
微服務的介紹與CRUD的實現
微服務就是把後端服務拆分為多個微小服務,來防止各服務之間的領域入侵,更能有效的開發和後期維護等
微服務一:使用者登入實戰(auth)
先來看看微信小程式登入的時序圖:
這裡的流程其實很清晰,我們看到小程式傳送code至開發者伺服器,接著開發者伺服器需要呼叫相關的方法和api去攜帶appid, appsecret和code上傳至微信相關服務去換取session_key和openid。
那麼重點來了:這裡我們不將openid直接與自定義登入態關聯,而是需要將我們拿到的openid進行儲存,拿出該openid在資料庫中的索引(統一叫id),與自定義登入態關聯,然後往下驗證。
所以這裡我們涉及的內容就是:
- 如何將openid存入資料庫中
- 如何建立openid的索引id
- 如何拿出索引id
- 如何給定id拿出openid
下面我們來實現:
這需要先了解一下MongoDB的索引,即:”_id”, 該欄位必須接受的是一個primitive.ObjectID型別的值,所以在使用是就需要涉及到型別轉換了。
我們直接將所有的型別轉換方法放入一個包中:objid
package objid
import (
"coolcar/shared/id"
"fmt"
"go.mongodb.org/mongo-driver/bson/primitive"
)
//ToAccount將 primitive.ObjectID轉換為string id
func ToAccountID(oid primitive.ObjectID) id.AccountID {
return id.AccountID(oid.Hex())
}
這裡還有一個問題我們傳入的是一個openid是string型別,我們拿出的_id也需要轉換為string型別,返回出來,萬一我們把openid和_id弄反了怎麼辦,大家都是string,編輯器也不會提示,所以這裡需要做強化型別處理。
強型別化包:id
package id
//強型別化: AccountID定義account id物件型別
type AccountID string
func (a AccountID) String() string {
return string(a)
}
這樣我們拿出的_id轉換成一個AccountID型別
接下來我們來回答這四個問題:
- 如何將openid存入資料庫中
- 如何建立openid的索引id
- 如何拿出索引id
- 如何給定id拿出openid
方法宣告:
func (*mongo.Collection).FindOneAndUpdate(ctx context.Context, filter interface{}, update interface{}, opts ...*options.FindOneAndUpdateOptions) *mongo.SingleResult
介紹:
FindOneAndUpdate 執行 findAndModify 命令以更新集合中的最多一個文件,並返回更新前的文件。
完整實現:見註釋
package dao
import (
"context"
"coolcar/shared/id"
"coolcar/shared/mongo/objid"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
IDFieldName = "_id" //存入資料庫的欄位名
openidfield = "open_id"
)
//定義一個 Mongo 型別
type Mongo struct {
col *mongo.Collection
}
//初始化資料庫, 類似建構函式
func NewMongo(db *mongo.Database) *Mongo {
return &Mongo{
col: db.Collection("auth"),
}
}
//將openID存入資料庫,返回對應_id給使用者
func (m *Mongo) ResolveAccountID(c context.Context, openID string) (id.AccountID, error) {
//篩選器,以openID為篩選條件
filter := bson.M{
openidfield: openID,
}
//生成一個primitive.ObjectID型別作為文件索引
var insertedID primitive.ObjectID
//更新的資料
updata := bson.M{
"$setOnInsert": bson.M{
IDFieldName: insertedID,
openidfield: openID,
},
}
//去查詢openID,如果查到的openID則將對應_id返回出來,沒有openID則插入我們固定的insertedID,然後將對應_id返回出來
res := m.col.FindOneAndUpdate(c, filter, updata, options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After))
//檢測是否返回成功
if err := res.Err(); err != nil {
return "", fmt.Errorf("cannot findOneAndUpdate: %v", err)
}
//解碼格式,我們在解碼的時候必須確定資料結構
var row struct{
ID primitive.ObjectID `bson:"_id"`
}
//解碼
err := res.Decode(&row)
if err != nil {
return "", fmt.Errorf("cannot Decode result: %v", err)
}
//做型別轉換,再返回
return objid.ToAccountID(row.ID), nil
}
這樣我們就完成了第一個登入服務的CRUD
接下來開始第二個服務的實戰吧!
微服務二:行程服務(trip)
在微服務二中我們需要做四件事情
- 建立行程
- 獲取單個行程
- 根據條件批次獲取行程
- 更新行程
在該服務中,我們同樣需要做型別轉換, 強型別化:
型別轉換:
package objid
import (
"coolcar/shared/id"
"fmt"
"go.mongodb.org/mongo-driver/bson/primitive"
)
//FromID將一個id轉換為Object id
func FromID(id fmt.Stringer) (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(id.String())
}
//MustFromID將一個id轉換為Object id
func MustFromID(id fmt.Stringer) primitive.ObjectID {
oid, err := FromID(id)
if err != nil {
panic(err)
}
return oid
}
//ToAccount將 primitive.ObjectID轉換為string id
func ToAccountID(oid primitive.ObjectID) id.AccountID {
return id.AccountID(oid.Hex())
}
//ToTripID將 primitive.ObjectID轉換為string id
func ToTripID(oid primitive.ObjectID) id.TripID {
return id.TripID(oid.Hex())
}
強型別化:
package id
//強型別化: AccountID定義account id物件型別
type AccountID string
func (a AccountID) String() string {
return string(a)
}
//TripID 定義一個trip id
type TripID string
func (t TripID) String() string {
return string(t)
}
//Identity定義一個使用者身份
type IdentityID string
func (i IdentityID) String() string {
return string(i)
}
//CarId定義一個車輛id
type CarId string
func (c CarId) String() string {
return string(c)
}
這一節中我們將大量使用到MongoDB的知識,所以我們將一些可以程式碼作為公共程式碼(微服務三也會用到),這樣我們就可以將CRUD中的變數,常量全都提出。
mgo:
package mgo
import (
"coolcar/shared/mongo/objid"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
IDFieldName = "_id"
UpdatedAtFieldName = "updatedat"
)
//ObjID defines the object field
type IDField struct {
ID primitive.ObjectID `bson:"_id"`
}
//UpdatedAtField 定義一個時間篩選器
type UpdatedAtField struct {
UpdatedAt int64 `bson:"updatedat"`
}
//NewObjectID 生成一個object id , NewObjID是一個函式
var NewObjID = primitive.NewObjectID
//NewObjIDWithValue 生成id 為下一個NewObjID,對id進一步包裝,
func NewObjIDWithValue(id fmt.Stringer) {
NewObjID = func() primitive.ObjectID {
return objid.MustFromID(id)
}
}
//Updateda 返回一個合適的值,你賦值給它
var UpdatedAt = func() int64 {
return time.Now().UnixNano() //當前時間取納秒
}
//Set return a $set updata document
func Set(V interface{}) bson.M {
return bson.M{
"$set": V,
}
}
func SetInsert(V interface{}) bson.M {
return bson.M{
"$setOnInsert": V,
}
}
注意:_id為每一個行程記錄的索引,即行程ID,每一個使用者也會有一個accountID,他們存放在一條文件中,但行程ID為資料庫文件索引。
完成準備工作開始CRUD:
建立行程
//欄位 const ( tripField = "trip" accountIDField = tripField + ".accountid" statusField = tripField + ".status" ) //定義資料儲存結果 type TripRecord struct { mgo.IDField `bson:"inline"` mgo.UpdatedAtField `bson:"inline"` //時間戳 Trip *rentalpb.Trip `bson:"trip"` } //建立行程, 將初始化資料放入資料庫中並分配Trip ID和時間戳 func (m *Mongo) CreateTrip(c context.Context, trip *rentalpb.Trip) (*TripRecord, error) { r := &TripRecord{ Trip: trip, } r.ID = mgo.NewObjID() r.UpdatedAt = mgo.UpdatedAt() _, err := m.col.InsertOne(c, r) if err != nil { return nil, err } return r, nil }
獲取當個行程
//根據條件獲取行程資訊 func (m *Mongo) GetTrip(c context.Context, id id.TripID, accountId id.AccountID) (*TripRecord, error) { //將id做型別轉換 ojbid, err := objid.FromID(id) if err != nil { return nil, fmt.Errorf("不能將id轉換: %v", err) } //註釋為另一種寫法 // filter := bson.M{ // "id": ojbid, // "trip.accountid": accountId, // } // res := m.col.FindOne(c, filter) //需要根據tripID和accountID進行篩選 res := m.col.FindOne(c, bson.M{ mgo.IDFieldName: ojbid, accountIDField: accountId, }) //將res以TripRecord的結構解碼 var tr TripRecord err = res.Decode(&tr) if err != nil { fmt.Errorf("不能解碼: %v", err) } return &tr, nil }
根據條件批次獲取行程
這裡我們還需要根據行程狀態(未開始, 進行中, 已完成)進行獲取
//GetTrips 根據條件去批次獲取使用者的行程資訊 func (m *Mongo) GetTrips(c context.Context, accountID id.AccountID, status rentalpb.TripStatus) ([]*TripRecord, error) { filter := bson.M{ accountIDField: accountID.String(), } if status != rentalpb.TripStatus_TS_NOT_SPECIFIED { filter[statusField] = status } res, err := m.col.Find(c, filter, options.Find().SetSort(bson.M{ mgo.IDFieldName: -1, })) if err != nil { return nil, fmt.Errorf("cannot Find matching documents: %v", err) } var trips []*TripRecord for res.Next(c) { //將res以TripRecord的結構解碼 var trip TripRecord err := res.Decode(&trip) if err != nil { fmt.Errorf("不能解碼: %v", err) } trips = append(trips, &trip) } return trips, nil }
更新行程
//UpdateTrip 根據輸入更新資料 func (m *Mongo) UpdateTrip(c context.Context, tripid id.TripID, accountid id.AccountID, updatedAt int64, trip *rentalpb.Trip) error { ojbid, err := objid.FromID(tripid) if err != nil { //fmt.Errorf("型別轉換失敗: %v", err) return err } newUpdateAt := mgo.UpdatedAt() //篩選器,根據行程ID,使用者ID和行程的時間戳進行篩選 filter := bson.M{ mgo.IDFieldName: ojbid, accountIDField: accountid.String(), mgo.UpdatedAtFieldName: updatedAt, } //更新資料 change := mgo.Set(bson.M{ tripField: trip, mgo.UpdatedAtFieldName: newUpdateAt, }) res, err := m.col.UpdateOne(c, filter, change) if err != nil { return err } if res.MatchedCount == 0 { return mongo.ErrNoDocuments } return nil }
這就是整個微服務二的CRUD實戰內容,下面是完整程式碼:
package dao import ( "context" rentalpb "coolcar/rental/api/gen/v1" "coolcar/shared/id" mgo "coolcar/shared/mongo" objid "coolcar/shared/mongo/objid" "fmt" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) const ( tripField = "trip" accountIDField = tripField + ".accountid" statusField = tripField + ".status" ) //定義一個 Mongo 型別 type Mongo struct { col *mongo.Collection } //初始化資料庫, 類似建構函式 func NewMongo(db *mongo.Database) *Mongo { return &Mongo{ col: db.Collection("trip"), } } type TripRecord struct { mgo.IDField `bson:"inline"` mgo.UpdatedAtField `bson:"inline"` //時間戳 Trip *rentalpb.Trip `bson:"trip"` } //建立行程, 將初始化資料放入資料庫中並分配Trip ID和時間戳 func (m *Mongo) CreateTrip(c context.Context, trip *rentalpb.Trip) (*TripRecord, error) { r := &TripRecord{ Trip: trip, } r.ID = mgo.NewObjID() r.UpdatedAt = mgo.UpdatedAt() _, err := m.col.InsertOne(c, r) if err != nil { return nil, err } return r, nil } //根據條件獲取行程資訊 func (m *Mongo) GetTrip(c context.Context, id id.TripID, accountId id.AccountID) (*TripRecord, error) { //將id做型別轉換 ojbid, err := objid.FromID(id) if err != nil { return nil, fmt.Errorf("不能將id轉換: %v", err) } // filter := bson.M{ // "id": ojbid, // "trip.accountid": accountId, // } // res := m.col.FindOne(c, filter) res := m.col.FindOne(c, bson.M{ mgo.IDFieldName: ojbid, accountIDField: accountId, }) //將res以TripRecord的結構解碼 var tr TripRecord err = res.Decode(&tr) if err != nil { fmt.Errorf("不能解碼: %v", err) } return &tr, nil } //GetTrips 根據條件去批次獲取使用者的行程資訊 func (m *Mongo) GetTrips(c context.Context, accountID id.AccountID, status rentalpb.TripStatus) ([]*TripRecord, error) { filter := bson.M{ accountIDField: accountID.String(), } if status != rentalpb.TripStatus_TS_NOT_SPECIFIED { filter[statusField] = status } res, err := m.col.Find(c, filter, options.Find().SetSort(bson.M{ mgo.IDFieldName: -1, })) if err != nil { return nil, fmt.Errorf("cannot Find matching documents: %v", err) } var trips []*TripRecord for res.Next(c) { //將res以TripRecord的結構解碼 var trip TripRecord err := res.Decode(&trip) if err != nil { fmt.Errorf("不能解碼: %v", err) } trips = append(trips, &trip) } return trips, nil } //UpdateTrip 根據輸入更新資料 func (m *Mongo) UpdateTrip(c context.Context, tripid id.TripID, accountid id.AccountID, updatedAt int64, trip *rentalpb.Trip) error { ojbid, err := objid.FromID(tripid) if err != nil { //fmt.Errorf("型別轉換失敗: %v", err) return err } //篩選器 newUpdateAt := mgo.UpdatedAt() filter := bson.M{ mgo.IDFieldName: ojbid, accountIDField: accountid.String(), mgo.UpdatedAtFieldName: updatedAt, } //更改資料 change := mgo.Set(bson.M{ tripField: trip, mgo.UpdatedAtFieldName: newUpdateAt, }) res, err := m.col.UpdateOne(c, filter, change) if err != nil { return err } if res.MatchedCount == 0 { return mongo.ErrNoDocuments } return nil }
微服務三:身份資訊的驗證(profile)
profile服務應該是和trip服務是一個微服務的,因為這裡需要涉及到blob微服務,所以我將profile單獨介紹,該服務的作用是將使用者上傳的身份資訊程式儲存和獲取,以及和另一個blob微服務進行互動
這裡我們先來實現使用者身份儲存CRUD:
通樣我們需要型別轉換:
package objid
import (
"coolcar/shared/id"
"fmt"
//"strings"
"go.mongodb.org/mongo-driver/bson/primitive"
)
//FromID將一個id轉換為Object id
func FromID(id fmt.Stringer) (primitive.ObjectID, error) {
return primitive.ObjectIDFromHex(id.String())
}
//MustFromID將一個id轉換為Object id
func MustFromID(id fmt.Stringer) primitive.ObjectID {
oid, err := FromID(id)
if err != nil {
panic(err)
}
return oid
}
//ToAccount將 primitive.ObjectID轉換為string id
func ToAccountID(oid primitive.ObjectID) id.AccountID {
return id.AccountID(oid.Hex())
}
//ToTripID將 primitive.ObjectID轉換為string id
func ToTripID(oid primitive.ObjectID) id.TripID {
return id.TripID(oid.Hex())
}
強型別化:
package id
//強型別化: AccountID定義account id物件型別
type AccountID string
func (a AccountID) String() string {
return string(a)
}
//TripID 定義一個trip id
type TripID string
func (t TripID) String() string {
return string(t)
}
//Identity定義一個使用者身份
type IdentityID string
func (i IdentityID) String() string {
return string(i)
}
//CarId定義一個車輛id
type CarId string
func (c CarId) String() string {
return string(c)
}
//BlobID定義一個blobID
type BlobID string
func (b BlobID) String() string {
return string(b)
}
公共程式碼:
package mgo
import (
"coolcar/shared/mongo/objid"
"fmt"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
IDFieldName = "_id"
UpdatedAtFieldName = "updatedat"
)
//ObjID defines the object field
type IDField struct {
ID primitive.ObjectID `bson:"_id"`
}
//UpdatedAtField 定義一個時間篩選器
type UpdatedAtField struct {
UpdatedAt int64 `bson:"updatedat"`
}
//NewObjectID 生成一個object id , NewObjID是一個函式
var NewObjID = primitive.NewObjectID
//NewObjIDWithValue 生成id 為下一個NewObjID,對id進一步包裝,
func NewObjIDWithValue(id fmt.Stringer) {
NewObjID = func() primitive.ObjectID {
return objid.MustFromID(id)
}
}
//Updateda 返回一個合適的值,你賦值給它
var UpdatedAt = func() int64 {
return time.Now().UnixNano() //當前時間取納秒
}
//Set return a $set updata document
func Set(V interface{}) bson.M {
return bson.M{
"$set": V,
}
}
func SetInsert(V interface{}) bson.M {
return bson.M{
"$setOnInsert": V,
}
}
//ZeroOrDoesNotExist是一個生成篩選器的表示式去篩選zero或者不存在的值
func ZeroOrDoesNotExist(field string, zero interface{}) bson.M {
return bson.M{
"$or": []bson.M{
{
field: zero,
},
{
field: bson.M{
"$exists": false,
},
},
},
}
}
兩方法個請求:
- 獲取身份資訊
- 更新身份資訊
- 更新個人資料照片和blob id
package dao
import (
"context"
rentalpb "coolcar/rental/api/gen/v1"
"coolcar/shared/id"
mgo "coolcar/shared/mongo"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
accountIDField = "accountid"
profileField = "profile"
identityStatusField = profileField + ".identitystatus"
photoblobIDField = "photoblobid"
)
type Mongo struct {
col *mongo.Collection
}
//建構函式
func NewMongo(db *mongo.Database) *Mongo {
return &Mongo{
col: db.Collection("profile"),
}
}
//ProfileRecord定義profile在資料庫中的解碼方式
type ProfileRecord struct {
AccountID string `bson:"accountid"`
Profile *rentalpb.Profile `bson:"profile"`
PhotoBlobID string `bson:"photoblobid"`
}
//獲取身份資訊
func (m *Mongo) GetProfile(c context.Context, aid id.AccountID) (*ProfileRecord, error) {
filter := bson.M{
accountIDField: aid.String(),
}
res := m.col.FindOne(c, filter)
//如果文件為空
if err := res.Err(); err != nil {
return nil, err
}
//對res進行解碼
var pr ProfileRecord
err := res.Decode(&pr)
if err != nil {
return nil, fmt.Errorf("解碼失敗: %v", err)
}
return &pr, nil
}
//更新身份資訊
func (m *Mongo) UpdateProfile(c context.Context, aid id.AccountID, prevState rentalpb.IdentityStatus, p *rentalpb.Profile) error {
filter := bson.M{
identityStatusField: prevState,
}
if prevState == rentalpb.IdentityStatus_UNSUBMITTED {
filter = mgo.ZeroOrDoesNotExist(identityStatusField, prevState)
}
filter[accountIDField] = aid.String()
change := mgo.Set(bson.M{
accountIDField: aid.String(),
profileField: p,
})
_, err := m.col.UpdateOne(c, filter, change,
options.Update().SetUpsert(true))
if err != nil {
return fmt.Errorf("更新失敗:%v", err)
}
return nil
}
//UpdateProfilePhoto 更新個人資料照片和blob id。
func (m *Mongo) UpdateProfilePhoto(c context.Context, aid id.AccountID, bid id.BlobID) error {
filter := bson.M{
accountIDField: aid.String(),
}
change := mgo.Set(bson.M{
accountIDField: aid.String(),
photoblobIDField: bid.String(),
})
_, err := m.col.UpdateOne(c, filter, change,
options.Update().SetUpsert(true))
if err != nil {
return fmt.Errorf("更新失敗:%v", err)
}
return err
}
微服務blob:工作流程圖:
blob根據profile提供的accountID,並根據blob資料庫該文件的索引和提供的accountID生成path,然後將一起存入資料庫,再返回對應資料, 圖片上傳完成後,profile向blob提供blobID(即:blbo資料庫文件索引),就可以拿到path,去雲端獲取圖片了,下面來實現blob的CRUD:
同樣型別轉換,強化型別和mgo和profile中一致
CRUD:
- 建立一個blod記錄
- 根據blobID或者資訊
package dao
import (
"context"
"coolcar/shared/id"
mgo "coolcar/shared/mongo"
"coolcar/shared/mongo/objid"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type Mongo struct {
col *mongo.Collection
}
//建構函式
func NewMongo(db *mongo.Database) *Mongo {
return &Mongo{
col: db.Collection("blob"),
}
}
type BlobRecord struct {
mgo.IDField `bson:"inline"`
AccountID string `bson:"accountid"`
Path string `bson:"path"`
}
//CreateBlob建立一個blod記錄
func (m *Mongo) CreateBlob(c context.Context, aid id.AccountID) (*BlobRecord, error) {
br := &BlobRecord{
AccountID: aid.String(),
}
objID := mgo.NewObjID()
br.ID = objID
br.Path = fmt.Sprintf("%s/%s", aid, objID.Hex())
fmt.Printf("MYRUL:%s\n", br.Path)
_, err := m.col.InsertOne(c, br)
if err != nil {
return nil, err
}
return br, nil
}
//根據blobID或者資訊
func (m *Mongo) GetBlob(c context.Context, bid id.BlobID) (*BlobRecord, error) {
objID, err := objid.FromID(bid)
if err != nil {
return nil, fmt.Errorf("失效的objid id: %v", err)
}
filter := bson.M{
mgo.IDFieldName: objID,
}
res := m.col.FindOne(c, filter)
if err = res.Err(); err != nil {
return nil, err
}
var br BlobRecord
err = res.Decode(&br)
if err != nil {
return nil, fmt.Errorf("解碼失敗: %v", err)
}
return &br, nil
}
這裡我們的幾個微服務都做完了
總結
這就是Go和MongoDB在實戰中是應用, 其實也很簡單,我們需要注意資料庫的索引_id的使用,以及各欄位的寫入,解碼結構,和資料型別的強型別化,雖然看上去增加了程式碼的量,但是這保證了我們資料不會出問題。
本作品採用《CC 協議》,轉載必須註明作者和本文連結