Go+MongoDB的微服务实战
介绍
本文章我们来学习一下使用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 协议》,转载必须注明作者和本文链接
推荐文章: