拉取鏡像git
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
啓動github
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
由於爲少
的開發環境是 VS Code
,因此安裝一下它(開發時,用它足夠了)。docker
開發時,咱們能夠點擊 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()
一句話描述:微信
account
集合中查找用戶 open_id
是否存在,存在就直接返回當前記錄,不存在就插入並返回當前插入的記錄。對應數據庫操做指令就是以下:app
db.account.findAndModify({ query: { open_id: "abcdef" }, update: { $setOnInsert: { _id: ObjectId("607132dcfbe32307260f728a"), open_id: "abcdef" } }, upsert: true, new: true // 返回新插入的記錄 })
注意:tcp
upsert
設爲 true
。知足查詢條件的記錄存在時,不執行 $setOnInsert
中的操做。知足條件的記錄不存在時,執行 $setOnInsert
操做。具體源碼放在(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
操做 Docker
容器進行單元測試。拒絕 Mock
,即時搭建/銷燬真實的 DAO Unit Tests
環境。
具體源碼放在(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
我是爲少 微信:uuhells123 公衆號:黑客下午茶 加我微信(互相學習交流),關注公衆號(獲取更多學習資料~)