系列
鑑權微服務資料持久化
使用 Docker 快速本地搭建 MongoDB 4.4.5 環境
拉取映象
docker pull mongo:4.4.5
# ....
# Digest: sha256:67018ee2847d8c35e8c7aeba629795d091f93c93e23d3d60741fde74ed6858c4
# Status: Image is up to date for mongo:4.4.5
# docker.io/library/mongo:4.4.5
啟動
docker run -p 27017:27017 -d mongo:4.4.5
docker ps
# e6e8e350e749 mongo:4.4.5 ... 0.0.0.0:27017->27017/tcp ...
OK,我們看到成功對映了容器埠(27017/tcp
)到了本機的 :27017
。
MongoDB for VS Code
因為為少
的開發環境是 VS Code
,所以安裝一下它(開發時,用它足夠了)。
使用 Playground 對 MongoDB 進行 CRUD
開發時,我們可以點選 Create New Playground
按鈕,進行資料庫相關的 CRUD
操作。
初始化資料庫和表
這裡,資料庫是grpc-gateway-auth
,表是account
。
use('grpc-gateway-auth');
db.account.drop()
db.account.insertMany([
{open_id: '123'},
{open_id: '456'},
])
db.account.find()
使用者 OpenID 查詢/插入業務邏輯(MongoDB 指令分析)
一句話描述:
- 在
account
集合中查詢使用者open_id
是否存在,存在就直接返回當前記錄,不存在就插入並返回當前插入的記錄。
對應資料庫操作指令就是如下:
db.account.findAndModify({
query: {
open_id: "abcdef"
},
update: {
$setOnInsert: {
_id: ObjectId("607132dcfbe32307260f728a"),
open_id: "abcdef"
}
},
upsert: true,
new: true // 返回新插入的記錄
})
注意:
- 將
upsert
設為true
。滿足查詢條件的記錄存在時,不執行$setOnInsert
中的操作。滿足條件的記錄不存在時,執行$setOnInsert
操作。
編碼實戰
為微服務提供一個輕量級 DAO
具體原始碼放在(dao/mongo
):
.......
.......
type Mongo struct {
col *mongo.Collection
newObjID func() primitive.ObjectID
}
func NewMongo(db *mongo.Database) *Mongo {
// 返回個引用出去,根據需要(測試時)外部可隨時改 `col` 和 `newObjID` 值
return &Mongo{
col: db.Collection("account"), // 給個初值
newObjID: primitive.NewObjectID,
}
}
.......
.......
編寫具體的查詢/插入業務邏輯
通過 OpenID
查詢關聯的賬號 ID
。具體原始碼放在(dao/mongo
):
func (m *Mongo) ResolveAccountID(c context.Context, openID string) (string, error) {
insertedID := m.newObjID()
// 對標上面的查詢/插入指令
res := m.col.FindOneAndUpdate(c, bson.M{
openIDField: openID,
}, mgo.SetOnInsert(bson.M{
mgo.IDField: insertedID, // mgo.IDField -> "_id",
openIDField: openID, // openIDField -> "open_id"
}), options.FindOneAndUpdate().
SetUpsert(true).
SetReturnDocument(options.After))
if err := res.Err(); err != nil {
return "", fmt.Errorf("cannot findOneAndUpdate: %v", err)
}
var row mgo.ObjID
err := res.Decode(&row)
if err != nil {
return "", fmt.Errorf("cannot decode result: %v", err)
}
return row.ID.Hex(), nil
}
Go 操作容器搭建真實的持久化 Unit Tests 環境
Go
操作 Docker
容器進行單元測試。拒絕 Mock
,即時搭建/銷燬真實的 DAO Unit Tests
環境。
單元測試期間,使用 Go 程式完成容器啟動與銷燬
具體原始碼放在(dao/mongo.go
):
func RunWithMongoInDocker(m *testing.M, mongoURI *string) int {
c, err := client.NewClientWithOpts()
if err != nil {
panic(err)
}
ctx := context.Background()
resp, err := c.ContainerCreate(ctx, &container.Config{
Image: image,
ExposedPorts: nat.PortSet{
containerPort: {},
},
}, &container.HostConfig{
PortBindings: nat.PortMap{
containerPort: []nat.PortBinding{
{
HostIP: "0.0.0.0", // 127.0.0.1
HostPort: "0", // 隨機挑一個埠
},
},
},
}, nil, nil, "")
if err != nil {
panic(err)
}
containerID := resp.ID
defer func() {
err := c.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{Force: true})
if err != nil {
panic(err)
}
}()
err = c.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
if err != nil {
panic(err)
}
inspRes, err := c.ContainerInspect(ctx, containerID)
if err != nil {
panic(err)
}
hostPort := inspRes.NetworkSettings.Ports[containerPort][0]
*mongoURI = fmt.Sprintf("mongodb://%s:%s", hostPort.HostIP, hostPort.HostPort)
return m.Run()
}
編寫表格驅動單元測試
具體原始碼放在(dao/mongo_test.go
):
func TestResolveAccountID(t *testing.T) {
c := context.Background()
mc, err := mongo.Connect(c, options.Client().ApplyURI(mongoURI))
if err != nil {
t.Fatalf("cannot connect mongodb: %v", err)
}
m := NewMongo(mc.Database("grpc-gateway-auth"))
// 初始化兩條資料
_, err = m.col.InsertMany(c, []interface{}{
bson.M{
mgo.IDField: mustObjID("606f12ff0ba74007267bfeee"),
openIDField: "openid_1",
},
bson.M{
mgo.IDField: mustObjID("606f12ff0ba74007267bfeef"),
openIDField: "openid_2",
},
})
if err != nil {
t.Fatalf("cannot insert initial values: %v", err)
}
// 注意,我猛將 `newObjID` 生成的 ID 變成固定了~
m.newObjID = func() primitive.ObjectID {
return mustObjID("606f12ff0ba74007267bfef0")
}
// 定義表格測試 case
cases := []struct {
name string
openID string
want string
}{
{
name: "existing_user",
openID: "openid_1",
want: "606f12ff0ba74007267bfeee",
},
{
name: "another_existing_user",
openID: "openid_2",
want: "606f12ff0ba74007267bfeef",
},
{
name: "new_user",
openID: "openid_3",
want: "606f12ff0ba74007267bfef0",
},
}
for _, cc := range cases {
t.Run(cc.name, func(t *testing.T) {
id, err := m.ResolveAccountID(context.Background(), cc.openID)
if err != nil {
t.Errorf("failed resolve account id for %q: %v", cc.openID, err)
}
if id != cc.want {
t.Errorf("resolve account id: want: %q; got: %q", cc.want, id)
}
})
}
}
func mustObjID(hex string) primitive.ObjectID {
objID, err := primitive.ObjectIDFromHex(hex)
if err != nil {
panic(err)
}
return objID
}
func TestMain(m *testing.M) {
os.Exit(mongotesting.RunWithMongoInDocker(m, &mongoURI))
}
執行測試
我們點選測試函式(TestResolveAccountID
)上方的 run test
我們看到多出來一個 Mongo DB
容器。
聯調
測試通過後,一般聯調是沒有問題的。
具體程式碼 auth/auth/auth.go
type Service struct {
Mongo *dao.Mongo // 肚子裡多一個資料訪問層
Logger *zap.Logger
OpenIDResolver OpenIDResolver
authpb.UnimplementedAuthServiceServer
}
func (s *Service) Login(c context.Context, req *authpb.LoginRequest) (*authpb.LoginResponse, error) {
s.Logger.Info("received code",
zap.String("code", req.Code))
openID, err := s.OpenIDResolver.Resolve(req.Code)
if err != nil {
return nil, status.Errorf(codes.Unavailable,
"cannot resolve openid: %v", err)
}
accountID, err := s.Mongo.ResolveAccountID(c, openID) // 查詢/插入操作
if err != nil {
s.Logger.Error("cannot resolve account id", zap.Error(err))
return nil, status.Error(codes.Internal, "")
}
return &authpb.LoginResponse{
AccessToken: "token for open id " + accountID,
ExpiresIn: 7200,
}, nil
}
具體程式碼 auth/main.go
authpb.RegisterAuthServiceServer(s, &auth.Service{
OpenIDResolver: &wechat.Service{
AppID: "your-app-id",
AppSecret: "your-app-secret",
},
Mongo: dao.NewMongo(mongoClient.Database("grpc-gateway-auth")),
Logger: logger,
})
執行
Service:
go run auth/main.go
gRPC-Gateway:
go run gateway/main.go
Refs
我是為少
微信:uuhells123
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)