简介
qmgo 是七牛云基于官方 mongo-go-driver 封装的 Go 操作 MongoDB 库,相比原生驱动更简洁、易用,贴合 Go 开发者的使用习惯。
mod
go 1.25.0
require (
github.com/bytedance/sonic v1.11.6
github.com/qiniu/qmgo v1.1.10
github.com/robfig/cron/v3 v3.0.1
)
客户端 初始化
go
package mgdb
import (
"context"
"log"
"github.com/qiniu/qmgo"
)
const (
Uri = "mongodb://127.0.0.1:27017"
Database = "wtt"
Username = "wtt"
Password = "PWY123123123"
AuthMechanism = "SCRAM-SHA-256"
AuthSource = "wtt"
)
var (
ctx = context.Background()
client *qmgo.Client
database *qmgo.Database
collection *qmgo.Collection
)
func init() {
InitMongoDbClient()
}
func InitMongoDbClient() {
c, err := qmgo.NewClient(ctx, &qmgo.Config{
Uri: Uri,
Database: Database,
Auth: &qmgo.Credential{
Username: Username,
Password: Password,
AuthMechanism: AuthMechanism,
AuthSource: AuthSource, // 认证数据库,通常是用户所在的数据库名
},
})
if err != nil {
log.Fatal("mongodb 初始化数据库失败:", err)
panic(err)
} else {
log.Println("mongodb 数据库初始化 成功")
}
client = c
database = client.Database("wtt")
collection = client.Database("wtt").Collection("whero")
}
func GetCollection() *qmgo.Collection {
if collection != nil {
return collection
} else {
InitMongoDbClient()
return collection
}
}
共用结构体
go
import (
"context"
"github.com/qiniu/qmgo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var ctx = context.Background()
var userColl = mgdb.GetCollection()
type User struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
Name string `bson:"name" json:"name"`
Age uint8 `bson:"age" json:"age"`
Email string `bson:"email,omitempty" json:"email"` // omitempty 表示如果email为空则不存入数据库
}
增
qmgo 支持单条插入和批量插入,核心方法是 InsertOne 和 InsertMany。
go
func insertOne(ctx context.Context, collection *qmgo.Collection) error {
user := &User{Name: "Alice", Age: 25, Email: "alice@example.com"}
res, err := collection.InsertOne(ctx, user)
fmt.Printf("%T---%+v\n", res, res)
return err
}
// 插入多条数据
func insertMany(ctx context.Context, collection *qmgo.Collection) error {
users := make([]*User, 0, 120)
for i := 0; i < 50; i++ {
var tmp = User{
Name: "whero" + GenerateRandomString(6),
Age: uint8(i),
Email: GenerateRandomString(10) + "@126.com",
}
users = append(users, &tmp)
}
_, err := collection.InsertMany(ctx, users)
return err
}
删
qmgo 支持单条删除、批量删除,核心方法是 DeleteOne/DeleteMany。
go
// 单条删除(根据姓名删除)
func deleteOneUser() {
// 构造删除条件
filter := bson.M{"name": "李四"}
// 执行删除
err := userColl.Remove(ctx, filter)
if err != nil {
log.Fatalf("单条删除失败: %v", err)
}
}
// 批量删除(年龄<30 的用户)
func deleteManyUsers() {
// 构造删除条件
filter := bson.M{"age": bson.M{"$gt": 20}}
// 执行批量删除
result, err := userColl.RemoveAll(ctx, filter)
if err != nil {
log.Fatalf("批量删除失败: %v", err)
}
fmt.Printf("批量删除了 %d 条文档\n", result.DeletedCount)
}
改
qmgo 支持单条更新、批量更新、替换文档、增量更新,核心方法是 UpdateOne/UpdateMany,更新语法使用 MongoDB 的更新操作符(set/set/set/inc 等)。
go
// 单条更新(更新张三的年龄和邮箱)
func updateOneUser() {
// 构造查询条件(找到要更新的文档)
filter := bson.M{"name": "张三"}
// 构造更新内容($set 表示设置字段,$inc 表示自增,$unset 表示删除字段等)
update := bson.M{
"$set": bson.M{
"age": 26, // 年龄改为26
"email": "zhangsan_new@example.com", // 邮箱更新
},
}
// 执行更新
err := userColl.UpdateOne(ctx, filter, update)
if err != nil {
log.Fatalf("单条更新失败: %v", err)
}
}
// 批量更新(年龄>25 的用户,年龄+1)
func updateManyUsers() {
// 构造查询条件
filter := qmgo.M{"age": qmgo.M{"$gt": 25, "$lt": 30}}
// 构造更新内容($inc 自增)
update := qmgo.M{
"$inc": qmgo.M{"age": 1}, // 年龄 分别 各自 +1
"$set": qmgo.M{
"email": "立正", // 邮箱 均设置为 立正
},
}
// 执行批量更新
result, err := userColl.UpdateAll(ctx, filter, update)
if err != nil {
log.Fatalf("批量更新失败: %v", err)
}
fmt.Printf("匹配到 %d 条文档,更新了 %d 条文档\n", result.MatchedCount, result.ModifiedCount)
}
// 替换文档(覆盖整个文档,除了 _id)
func replaceUser() {
// 构造查询条件
filter := bson.M{"name": "李四"}
// 构造新的文档(替换原有内容)
newUser := User{
Name: "李四",
Age: 29,
Email: "lisi_new@example.com",
}
// 执行替换
err := userColl.ReplaceOne(ctx, filter, newUser)
if err != nil {
log.Fatalf("替换文档失败: %v", err)
}
}
查
qmgo 的查询功能非常灵活,支持单条查询、批量查询、条件查询、分页、排序、字段过滤等,核心方法是 Find + One/All。
go
// 根据 ID 查询单条数据
func findUserByID(id string) {
// 将字符串 ID 转为 ObjectID
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
log.Fatalf("ID 格式错误: %v", err)
}
var user User
// 执行查询(Find 构造条件,One 取单条)
err = userColl.Find(ctx, bson.M{"_id": objectID}).One(&user)
if err != nil {
if err == qmgo.ErrNoSuchDocuments {
log.Println("未找到该用户")
return
}
log.Fatalf("查询失败: %v", err)
}
fmt.Printf("查询结果: %+v\n", user)
}
// 条件查询(年龄>25 且 姓名包含"李")
func findUsersByCondition() {
// 构造查询条件(bson.M 是原生 MongoDB 查询语法)
filter := bson.M{
"age": bson.M{"$gt": 25}, // 年龄 > 25
"name": bson.M{"$regex": "a"}, // 姓名包含"a"(正则匹配)
}
var users []User
// 执行查询(All 取多条结果)
err := userColl.Find(ctx, filter).All(&users)
if err != nil {
log.Fatalf("条件查询失败: %v", err)
}
fmt.Printf("查询到 %d 条数据:\n", len(users))
for _, u := range users {
fmt.Printf("%+v\n", u)
}
}
// 分页 + 排序查询(按创建时间降序,第1页,每页2条)
func findUsersWithPagination() {
page := 1 // 当前页
pageSize := 5 // 每页条数
skip := (page - 1) * pageSize
var users []User
// Find(条件) -> Sort(排序) -> Skip(跳过) -> Limit(限制条数) -> All(取结果)
err := userColl.Find(ctx, bson.M{"age": bson.M{"$gt": 25}}).
Sort("-age"). // 降序(- 表示降序,无则升序)
Skip(int64(skip)).
Limit(int64(pageSize)).
All(&users)
if err != nil {
log.Fatalf("分页查询失败: %v", err)
}
fmt.Printf("分页查询结果:\n")
for _, u := range users {
fmt.Printf("%+v\n", u)
}
}
// 字段过滤(只返回 name 和 age,不返回 _id)
func findUsersWithProjection() {
var users []User
// 使用 qmgo 的 Find 方法,Projection 定义返回的字段(1 表示返回,0 表示不返回)
err := userColl.Find(ctx, qmgo.M{}).Select(qmgo.M{
"name": 1,
"age": 1,
"_id": 0,
}).All(&users)
if err != nil {
log.Fatalf("查询失败: %v", err)
}
fmt.Printf("字段过滤查询结果:\n")
for _, u := range users {
fmt.Printf("姓名: %s, 年龄: %d\n", u.Name, u.Age)
}
}
事务
go
// 事务示例(插入+更新原子执行)
func transactionDemo() {
tx, err := client.Session()
if err != nil {
log.Fatalf("获取 Session 失败: %v", err)
}
// 务必在函数结束时关闭 Session
defer tx.EndSession(ctx)
// 2. 执行事务
// WithTransaction 会自动处理事务的提交和回滚
res, err := tx.StartTransaction(ctx, func(ctx context.Context) (any, error) {
// --- 事务开始 ---
// 事务内操作1:插入用户
user := User{Name: "赵六", Age: 32, Email: "zhaoliu@example.com"}
_, err := userColl.InsertOne(ctx, user)
if err != nil {
return "", err
}
// 事务内操作2:更新张三的年龄
filter := bson.M{"name": "张三"}
update := bson.M{"$set": bson.M{"age": 11, "email": "3333333333"}}
err = userColl.UpdateOne(ctx, filter, update) // 注意:UpdateOne 返回 (UpdateResult, error)
if err != nil {
return "", err
}
return "123", nil
})
if err != nil {
log.Fatalf("事务执行失败: %v", err)
}
fmt.Println(res)
fmt.Println("事务执行成功")
}
上面的代码执行 如果报错: (IllegalOperation) Transaction numbers are only allowed on a replica set member or mongos
它不是代码写错了,而是你的 MongoDB 数据库环境配置不支持事务。
MongoDB 的事务功能(Transactions)有严格的环境要求:
- 单机模式:默认安装的 MongoDB 通常是单机模式,不支持事务。
- 支持模式:只有 副本集 或 分片集群 才支持事务。
你当前的数据库是以单机模式运行的,所以当你尝试开启事务时,数据库直接拒绝了请求。
聚合查询
go
package main
import (
"context"
"fmt"
"gotests/mgdb"
"go.mongodb.org/mongo-driver/bson"
)
var ctx = context.Background()
var userColl = mgdb.GetCollection("users")
type User struct {
ID string `bson:"_id,omitempty" json:"id"`
Name string `bson:"name" json:"name"`
Age uint8 `bson:"age" json:"age"`
Email string `bson:"email,omitempty" json:"email"`
}
type Vips struct {
ID string `bson:"_id,omitempty" json:"id"`
Name string `bson:"name" json:"name"`
Age uint8 `bson:"age" json:"age"`
Email string `bson:"email,omitempty" json:"email"`
}
func main() {
err := aggregateLeftJoinUserOrders()
fmt.Println(err)
}
// 聚合左连接:用户表 LEFT JOIN 订单表
func aggregateLeftJoinUserOrders() error {
// 定义聚合管道(Pipeline)
pipeline := []bson.M{
// 第一步:关联订单表($lookup 是联表核心操作)
{
"$lookup": bson.M{
"from": "vips", // 要关联的集合名
"localField": "age", // 当前集合(users)的关联字段
"foreignField": "age", // 关联集合(vips)的关联字段
"as": "matchedVips", // 关联结果存储的字段名
},
},
// 第二步:可选 - 过滤掉没有订单的用户(如果需要 INNER JOIN 效果)
{
"$match": bson.M{
"matchedVips": bson.M{"$ne": []any{}}, // 排除订单为空的用户
},
},
// 第三步:unwind - 展开数组
{
"$unwind": "$matchedVips",
},
}
// 执行聚合查询
var users []User
err := userColl.Aggregate(ctx, pipeline).All(&users)
if err != nil {
return err
}
fmt.Println(len(users))
fmt.Printf("%T---%+v\n", users, users)
return nil
}
聚合查询 总数统计
go
func aggregateLeftJoinUserOrders() error {
// 定义聚合管道(Pipeline)
pipeline := []bson.M{
// 第一步:关联订单表($lookup 是联表核心操作)
{
"$lookup": bson.M{
"from": "vips", // 要关联的集合名
"localField": "age", // 当前集合(users)的关联字段
"foreignField": "age", // 关联集合(vips)的关联字段
"as": "matchedVips", // 关联结果存储的字段名
},
},
// 第二步:可选 - 过滤掉没有订单的用户(如果需要 INNER JOIN 效果)
{
"$match": bson.M{
"matchedVips": bson.M{"$ne": []any{}}, // 排除订单为空的用户
},
},
// 第三步:unwind - 展开数组
{
"$unwind": "$matchedVips",
},
// 第四步:count - 统计总数
{
"$count": "totalit",
},
}
// 执行聚合查询
var results []struct {
Totalit int `bson:"totalit"`
}
err := userColl.Aggregate(ctx, pipeline).All(&results)
if err != nil {
return err
}
fmt.Println(results[0].Totalit)
return nil
}
位置范围查询
go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/qiniu/qmgo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var collection = 初始化的文档变量
func main() {
res, err := queryNearbyUsers(ctx, collection, "用户A", 15)
if err != nil {
fmt.Println(err)
}
fmt.Println(res)
}
// User 用户模型(包含地理坐标)
type User struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` // 主键
Name string `bson:"name" json:"name"` // 用户名
Location struct { // 地理坐标(2dsphere 索引核心字段)
Type string `bson:"type" json:"type"` // 固定值:Point
Coordinates []float64 `bson:"coordinates" json:"coordinates"` // 格式:[经度, 纬度]
} `bson:"location" json:"location"`
}
// queryNearbyUsers 查询指定用户附近 N 公里的所有用户
// 参数:
//
// ctx: 上下文
// col: 集合对象
// userName: 目标用户名(如用户A)
// radiusKm: 搜索半径(公里)
func queryNearbyUsers(ctx context.Context, col *qmgo.Collection, userName string, radiusKm float64) ([]User, error) {
// 第一步:先查询用户A的经纬度
var targetUser User
err := col.Find(ctx, bson.M{"name": userName}).One(&targetUser)
if err != nil {
return nil, fmt.Errorf("查询目标用户失败:%v", err)
}
// 第二步:构造地理空间查询条件
// 注意:$maxDistance 单位是米,所以需要将公里转换为米
maxDistance := radiusKm * 1000
filter := bson.M{
"name": bson.M{"$ne": userName}, // 排除用户A自己
"location": bson.M{
"$near": bson.M{
"$geometry": bson.M{
"type": targetUser.Location.Type,
"coordinates": targetUser.Location.Coordinates,
},
"$maxDistance": maxDistance, // 单位:米
},
},
}
// 第三步:执行查询
var nearbyUsers []User
err = col.Find(ctx, filter).All(&nearbyUsers)
if err != nil {
return nil, fmt.Errorf("查询附近用户失败:%v", err)
}
return nearbyUsers, nil
}