连接MongoDB数据库
在Go语言中连接MongoDB,你可以使用官方的MongoDB Go Driver,即mongo-go-driver
。这个库提供了丰富的API来与MongoDB数据库进行交互。以下是一个基本的步骤和示例代码,展示如何在Go程序中连接到MongoDB数据库。
步骤 1: 安装 MongoDB Go Driver
首先,你需要安装MongoDB Go Driver。在你的Go项目目录下,通过运行以下命令来安装:
1
2
|
go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/mongo/options
|
步骤 2: 编写Go代码连接MongoDB
接下来,你可以编写Go代码来连接到MongoDB数据库。以下是一个简单的示例,展示了如何连接到MongoDB数据库并执行一个基本的查询。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
package main
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
// 设置MongoDB连接字符串
// 格式通常为:mongodb://username:password@host:port/dbname
uri := "mongodb://admin:admin@localhost:27017"
clientOptions := options.Client().ApplyURI(uri)
// 连接到MongoDB
client, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
log.Fatal(err)
}
// 检查连接
err = client.Ping(context.TODO(), nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected to MongoDB!")
// 选择数据库和集合
collection := client.Database("testdb").Collection("testcol")
// 插入文档
insertResult, err := collection.InsertOne(context.TODO(), bson.D{{"name", "MongoDB"}, {"type", "database"}, {"count", 1}, {"info", bson.D{{"x", 203}, {"y", 102}}}}})
if err != nil {
log.Fatal(err)
}
fmt.Println("Inserted a single document: ", insertResult.InsertedID)
// 查询文档
var result bson.M
err = collection.FindOne(context.TODO(), bson.D{{"name", "MongoDB"}}).Decode(&result)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found a single document: %+v\n", result)
// 关闭MongoDB连接
err = client.Disconnect(context.TODO())
if err != nil {
log.Fatal(err)
}
fmt.Println("Connection to MongoDB closed.")
}
|
context.Context参数
在MongoDB Go Driver中,许多操作都接受一个context.Context
参数。
可以使用context.Context
来管理连接MongoDB的请求生命周期,主要是为了能够控制操作的执行时间,防止长时间运行的操作阻塞程序,同时也能够在需要时取消这些操作。
下面是一个具体的示例,展示如何在与MongoDB交互时使用context.Context
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
func main() {
// 创建一个新的MongoDB客户端,使用默认的Context
client, _ := mongo.Connect(context.Background(), options.Client().ApplyURI("mongodb://localhost:27017"))
// 选择数据库和集合
collection := client.Database("mydatabase").Collection("users")
// 创建一个带有时限的Context,用于查询操作
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 使用Context发起查询,查找年龄大于30的用户
filter := bson.M{"age": bson.M{"$gt": 30}}
cursor, err := collection.Find(ctx, filter)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("Query timed out")
} else {
log.Fatal(err)
}
return
}
defer cursor.Close(ctx)
// 遍历查询结果
for cursor.Next(ctx) {
var user map[string]interface{}
if err := cursor.Decode(&user); err != nil {
log.Fatal(err)
}
fmt.Println(user)
}
// 检查是否有错误发生
if err := cursor.Err(); err != nil {
log.Fatal(err)
}
}
|
关键点解析
-
创建Context:使用context.WithTimeout
创建一个带有时限的Context,这个Context将在5秒后超时。
-
发起MongoDB操作:将创建的限时Context传递给collection.Find
方法,这样查询操作将受到超时限制。
-
处理超时:如果查询操作超时,ctx.Err()
将返回context.DeadlineExceeded
错误,可以通过检查这个错误来判断操作是否因超时而被取消。
-
取消Context:在查询操作完成后,调用cancel
函数来取消Context,释放资源。
bson.D和bson.M
在MongoDB的Go语言驱动(mongo-go-driver)中,bson.D
和bson.M
是两种用于表示BSON文档的Go类型。BSON(Binary JSON)是MongoDB中用于存储文档的一种二进制格式的JSON变种。这两种类型各有其用途和优势,下面详细说明它们之间的差异和用法。
bson.D
bson.D
是一个文档类型,用于表示有序的键值对序列。它是一个[]bson.E
的别名,其中bson.E
是一个结构体,包含Key
(字符串类型,键名)和Value
(interface{}
类型,键值)两个字段。bson.D
特别适用于那些需要明确控制BSON文档中字段顺序的场景,因为它保留了元素的插入顺序。
示例:
1
2
3
4
5
6
7
8
|
func main() {
doc := bson.D{
{"name", "John Doe"},
{"age", 30},
{"isStudent", false},
}
// ... 后续操作,如插入文档到MongoDB等
}
|
bson.M
bson.M
是一个映射类型,用于表示无序的键值对集合。它本质上是map[string]interface{}
的别名,因此使用起来非常类似于Go中的标准map。由于它是基于map的,所以它不保留元素的插入顺序。bson.M
在编写快速原型或当字段顺序不重要时非常有用。
示例:
1
2
3
4
5
6
7
8
|
func main() {
doc := bson.M{
"name": "Jane Doe",
"age": 28,
"isStudent": true,
}
// ... 后续操作,如插入文档到MongoDB等
}
|
使用场景差异
- 当你需要明确控制BSON文档中字段的顺序时,应该使用
bson.D
。这在某些情况下是必要的,比如MongoDB的某些操作可能会依赖于字段的顺序(尽管这种情况较少)。
- 当你不需要关心字段顺序,或者只是想快速编写代码时,
bson.M
是一个更自然、更简洁的选择。
查询
数据库名称为test
,集合名称为stus
,文档的结构为:
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"_id" : ObjectId("66869ef8abdfde5a12c58d50"),
"name" : "Alice",
"age" : 26,
"gender" : "female",
"class" : "computer science 1",
"score" : 85,
"hobbies" : {
"cities" : ["beijing", "shanghai", "shenzhen"],
"sports" : ["basketball", "tennis"]
}
}
|
先创建Go语言的映射结构体:
1
2
3
4
5
6
7
8
|
type Student struct {
Name string `bson:"name"`
Age int `bson:"age"`
Gender string `bson:"gender"`
Class string `bson:"class"`
Score int `bson:"score"`
// 其他字段...
}
|
然后在main
函数中连接数据库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func main() {
client, err := connectMongodb()
if err != nil {
panic(err)
}
defer func(client *mongo.Client, ctx context.Context) {
err := client.Disconnect(ctx)
if err != nil {
panic(err)
}
fmt.Println("Connection to MongoDB closed.")
}(client, context.TODO())
fmt.Println("Connected to MongoDB!")
// 获取集合
collection := client.Database("test").Collection("stus")
// 后面示例的代码写在后面
// ......
}
|
mongo.Collection.Find
方法是查询文档的主要方式之一。
方法签名
Go 语言中,mongo.Collection.Find
方法的基本签名如下:
1
2
|
func (coll *Collection) Find(ctx context.Context, filter interface{},
opts ...*options.FindOptions) (cur *Cursor, err error)
|
其中:
ctx context.Context
:上下文对象,用于控制请求的生命周期,例如设置超时或取消请求。
filter interface{}
:查询过滤器,可以是任何实现了 bson.Raw
或 Document
接口的对象,通常是一个 map[string]interface{} 结构,用于指定查询条件。
*options.FindOptions
:一个可选的参数,用于指定查询的附加选项,如排序、限制结果数量、跳过某些结果等。
返回值
Find
方法返回一个 *mongo.Cursor
对象,这是一个游标,可以迭代地访问查询结果中的文档。
示例1:查询 name=“Tom” 且 age>30 的第一个文档
既可以使用bson.D
,也可以使用bson.M
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 构建查询条件
// 使用bson.D类型
query1 := bson.D{
{"name", "Tom"},
{"age", bson.D{{"$gt", 30}}},
}
// 也可以使用bson.M类型
//query1 := bson.M{
// "name": "Tom",
// "age": bson.M{"$gt": 30},
//}
// 将查询的结果保存到stu中
var stu Student
err = collection.FindOne(context.TODO(), query1).Decode(&stu)
if err != nil {
log.Fatal(err)
}
fmt.Println(stu)
|
FindOne()
方法返回的是*mongo.SingleResult
类型,可以使用Decode()
方法,将其保存到Student
类型的变量中。
示例2:计算集合中所有文档的数量
查询条件为空,既可以使用bson.D
,也可以使用bson.M
。
1
|
count, _ := collection.CountDocuments(context.TODO(), bson.M{}) // 也可以使用bson.D{}
|
示例3:计算年龄大于30的文档数量
1
2
|
filter := bson.M{"age": bson.M{"$gt": 30}}
count, _ = collection.CountDocuments(context.TODO(), filter)
|
示例4:查询集合中所有的文档
1
2
3
4
5
6
7
8
9
10
11
|
filter = bson.M{} // 创建空过滤器
// 获取MongoDB集合中所有的文档,并将结果存储在一个游标cursor中
cursor, _ := collection.Find(context.TODO(), filter)
// 遍历游标,处理查询结果
for cursor.Next(context.TODO()) {
var stu Student
if err := cursor.Decode(&stu); err != nil {
log.Fatal(err)
}
fmt.Println(stu)
}
|
Find()
方法返回的是*mongo.Cursor
类型,可以cursor.Next()
方法来对游标cursor
进行遍历,在遍历到的游标位置,也可以调用Decode()
方法,将其保存到Student
类型的变量中。
示例5:查询分数在80到90的文档
1
2
|
filter = bson.M{"score": bson.M{"$gte": 80, "$lte": 90}}
cursor, _ = collection.Find(context.TODO(), filter)
|
示例6:或条件查询
查询name="Eric"
或age=36
的文档
1
2
3
4
5
6
7
|
filter = bson.M{
"$or": []bson.M{
{"name": "Eric"},
{"age": 36},
},
}
cursor, _ = collection.Find(context.TODO(), filter)
|
示例7:将所有文档按照score降序
如果是单一排序条件,即可以使用bson.M
,也可以使用bson.D
。
1
2
3
4
5
6
7
8
|
//sort := bson.M{
// "score": -1, // score降序
//}
sort := bson.D{
{"score", -1}, // score降序
}
opts := options.Find().SetSort(sort)
cursor, _ = collection.Find(context.TODO(), bson.M{}, opts)
|
示例8:将所有文档先按照score降序,再age升序
多排序条件,只能使用bson.D
而不能使用bson.M
,因为这里的排序是有先后顺序的。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 错误的写法:
//sort1 := bson.M{
// "score": -1, // score降序
// "age": 1, // age升序
//}
// 正确的写法:
sort1 := bson.D{
{"score", -1}, // score降序
{"age", 1}, // age升序
}
opts = options.Find().SetSort(sort1)
cursor, _ = collection.Find(context.TODO(), bson.M{}, opts)
|
示例9:只查询前 3 个文档
1
2
|
limitOpts := options.Find().SetLimit(3)
cursor, _ = collection.Find(context.TODO(), bson.M{}, limitOpts)
|
示例10:跳过前 3 个文档
1
2
|
skipOpts := options.Find().SetSkip(3)
cursor, _ = collection.Find(context.TODO(), bson.M{}, skipOpts)
|
示例11:设置投影选项,只返回 name 和 score 字段,排除 _id 字段
1
2
3
|
projection := bson.M{"name": 1, "score": 1, "_id": 0}
projectionOpts := options.Find().SetProjection(projection)
cursor, _ = collection.Find(context.TODO(), bson.M{}, projectionOpts)
|
示例12:in 条件查询
查询班级为computer science 1
和computer science 2
的文档:
1
2
3
4
5
6
|
filter = bson.M{
"class": bson.M{
"$in": []string{"computer science 1", "computer science 2"},
},
}
cursor, _ = collection.Find(context.TODO(), filter)
|
示例13:查询存在 hobbies 字段的文档
1
2
3
4
5
6
|
filter = bson.M{
"hobbies": bson.M{
"$exists": true,
},
}
cursor, _ = collection.Find(context.TODO(), filter)
|
插入
InsertOne
方法是 MongoDB Go 驱动程序中用于向集合中插入单个文档的函数。它是 mongo.Collection
类型的一个方法,允许你向指定的 MongoDB 集合中添加一个新文档。如果插入成功,它将返回一个包含新插入文档 ID 的结果,以及可能发生的任何错误。
InsertMany
方法的格式和InsertOne
方法相同。
方法签名
在 MongoDB Go 驱动程序中,InsertOne
方法的大致签名如下:
1
2
|
func (c *Collection) InsertOne(ctx context.Context, document interface{},
opts ...*options.InsertOneOptions) (*InsertOneResult, error)
|
ctx context.Context
:一个上下文对象,用于控制请求的生命周期,比如取消操作或设置超时时间。
document interface{}
:要插入的文档,它应该是一个可以被编码为 BSON 的值,如 bson.M
、bson.D
、结构体等。
opts ...*options.InsertOneOptions
:一个可选的参数列表,允许你指定额外的插入选项,比如绕过文档验证等。如果不需要这些选项,可以省略此参数。
返回值
*InsertOneResult
:一个包含插入操作结果的对象。通常,你会关心这个对象中的 InsertedID
字段,它包含了新插入文档的 _id
字段的值(如果文档本身没有指定 _id
,MongoDB 会自动生成一个)。
error
:如果在插入过程中发生错误,将返回一个错误对象。你应该检查这个返回值以确定操作是否成功。
示例1:插入单个文档
既可以使用bson.D
,也可以使用bson.M
。
使用bson.D
1
2
3
4
5
6
7
|
collection := client.Database("test").Collection("stus")
collection.InsertOne(context.TODO(), bson.D{
{"name", "张三"},
{"age", 20},
{"gender", "男"},
{"class", "计算机科学与技术1班"},
})
|
使用bson.M
1
2
3
4
5
6
7
|
collection.InsertOne(context.TODO(), bson.M{
"name": "李四",
"age": 23,
"gender": "男",
"class": "计算机科学与技术3班",
"score": 69,
})
|
示例2:插入多个文档
和插入单个文档相同,都可以使用bson.D
或bson.M
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// 使用bson.D
//docs := []interface{}{
// bson.D{
// {"name", "张三"},
// {"age", 20},
// {"gender", "男"},
// {"class", "计算机科学与技术1班"},
// },
// bson.D{
// {"name", "李四"},
// {"age", 21},
// {"gender", "男"},
// {"class", "计算机科学与技术2班"},
// },
//}
// 使用bson.M
docs := []interface{}{
bson.M{
"name": "张三",
"age": 20,
"gender": "男",
"class": "计算机科学与技术1班",
},
bson.M{
"name": "李四",
"age": 21,
"gender": "男",
"class": "计算机科学与技术2班",
},
}
collection.InsertMany(context.TODO(), docs)
|
更新
collection.UpdateOne()
方法是 MongoDB Go 驱动程序中用于更新集合中符合特定查询条件的第一个文档的函数。它是 mongo.Collection
类型的一个方法,提供了灵活的方式来修改集合中的文档。以下是该方法的详细说明:
方法签名
在 MongoDB Go 驱动程序中,UpdateOne
方法的大致签名如下(注意:具体签名可能会根据驱动程序的版本而有所不同):
1
2
|
func (c *Collection) UpdateOne(ctx context.Context, filter interface{},
update interface{}, opts ...*options.UpdateOptions) (*UpdateResult, error)
|
ctx context.Context
:一个上下文对象,用于控制请求的生命周期,比如取消操作或设置超时时间。
filter interface{}
:一个查询条件,用于指定要更新的文档。它应该是一个可以被编码为 BSON 的值,MongoDB 将使用这个条件来查找集合中的文档。
update interface{}
:一个更新操作,指定了如何修改找到的文档。它应该是一个 BSON 文档,可以包含更新操作符(如 $set
、$inc
等)来指定更新操作。
opts ...*options.UpdateOptions
:一个可选的参数列表,允许你指定额外的更新选项,比如是否进行乐观锁检查(通过版本字段)等。如果不需要这些选项,可以省略此参数。
返回值
示例1:将name=“Alice"的文档的score改为85
既可以使用bson.D
,也可以使用bson.M
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 获取集合
collection := client.Database("test").Collection("stus")
// 使用bson.D
// 构建查询条件
// filter := bson.D{{"name", "Alice"}}
// 构建更新操作
// update := bson.D{{"$set", bson.D{{"score", 85}}}}
// 使用bson.M
filter := bson.M{"name": "Alice"}
update := bson.M{"$set": bson.M{"score": 84}}
// 执行 updateOne 操作
result, _ := collection.UpdateOne(context.TODO(), filter, update)
// 打印更新结果
fmt.Printf("Matched %v documents and updated %v documents.\n", result.MatchedCount, result.ModifiedCount)
|
示例2:更新多个文档
1
2
3
|
filter = bson.M{"class": "computer science 1"}
update = bson.M{"$inc": bson.M{"score": -5}}
collection.UpdateMany(context.TODO(), filter, update)
|
示例3:替换整个文档
1
2
3
4
5
6
7
8
9
10
|
filter = bson.M{"name": "Denny"}
replacement := bson.M{
"name": "Dennis",
"age": 29,
"gender": "male",
"class": "computer science 1",
"score": 92,
}
// 执行 replaceOne 操作
collection.ReplaceOne(context.TODO(), filter, replacement)
|
示例4:使用 upsert 选项
1
2
3
4
5
6
7
8
9
10
11
12
|
filter = bson.M{"name": "Charlie", "age": 22}
update = bson.M{"$set": bson.M{
"name": "Charlie",
"age": 22,
"gender": "male",
"class": "computer science 3",
"score": 99,
}}
// 构建选项以启用upsert
opts := options.Update().SetUpsert(true)
// 执行updateOne操作
collection.UpdateOne(context.TODO(), filter, update, opts)
|
示例 5:从文档中移除指定的字段
1
2
3
4
5
6
7
8
|
filter = bson.M{"name": "Eric"}
// 构建更新操作,使用$unset来移除score字段
update = bson.M{"$unset": bson.M{"score": 1}}
// 注意:在$unset操作中,字段的值实际上并不重要,因为$unset是根据字段名来移除字段的。
// 但为了与MongoDB shell中的语法保持一致,这里仍然给出了值1。
// 在实际使用中,你也可以省略这个值,直接写bson.M{"score": ""}或完全忽略值(虽然这可能会影响代码的可读性)。
// 执行updateOne操作
collection.UpdateOne(context.TODO(), filter, update)
|
删除
collection.DeleteOne()
方法是 MongoDB Go 驱动程序中的一个函数,用于从 MongoDB 集合中删除一个与给定过滤器匹配的文档。这个方法是 *mongo.Collection
类型的一个方法,表示对 MongoDB 集合的操作。
函数签名
在 MongoDB Go 驱动程序中,DeleteOne
方法的函数签名大致如下(注意:具体签名可能会根据 MongoDB Go 驱动程序的版本而略有不同):
1
2
|
func (c *Collection) DeleteOne(ctx context.Context, filter interface{},
opts ...*options.DeleteOptions) (*DeleteResult, error)
|
ctx context.Context
:一个上下文对象,用于控制请求的取消、超时和截止日期。虽然 context.TODO()
可以用于简单的示例或测试,但在生产代码中,你应该传递一个具有适当超时和取消逻辑的上下文。
filter interface{}
:一个用于指定要删除的文档的选择器的接口。在 MongoDB Go 驱动程序中,这通常是一个 bson.M
或 bson.D
类型的值,用于构建查询条件。
opts ...*options.DeleteOptions
:一个可选的 *options.DeleteOptions
类型的切片,用于指定额外的删除选项,如写关注(write concern)或投影(虽然对于删除操作来说,投影并不适用)。
返回值
*DeleteResult
:一个指向 DeleteResult
结构的指针,该结构包含关于删除操作的信息,如被删除的文档数(对于 DeleteOne
来说,这将是 0 或 1)。
error
:如果在执行删除操作时发生错误,则返回该错误。
示例1:
示例 1:删除单个文档
要删除名为 “张三” 的学生的文档
1
2
3
4
5
6
7
|
// 获取集合
collection := client.Database("test").Collection("stus")
// 构建过滤器
filter := bson.M{"name": "张三"}
// 执行deleteOne操作
result, _ := collection.DeleteOne(context.TODO(), filter)
fmt.Println("删除文档的数量:", result.DeletedCount)
|
示例 2:删除多个文档
要删除所有名为 “张三” 的学生的文档
1
2
3
4
|
filter := bson.M{"name": "张三"}
// 执行deleteMany操作
deleteResult, _ := collection.DeleteMany(context.TODO(), filter)
fmt.Println("删除文档的数量:", deleteResult.DeletedCount)
|
示例 3:删除所有文档(慎用)
1
2
3
4
5
6
7
|
filter := bson.M{} // 构建一个空的查询过滤器
// 执行deleteMany操作以删除所有文档
deleteResult, _ = collection.DeleteMany(context.TODO(), filter)
fmt.Println("删除文档的数量:", deleteResult.DeletedCount)
// 或者使用drop()来删除集合
collection.Drop(context.TODO())
|
内嵌文档
示例1:给name=“Alice"的文档添加hobbies属性,hobbies也有两个属性:cities和sports
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 获取集合
collection := client.Database("test").Collection("stus")
// 构建查询过滤器
filter := bson.M{"name": "Tom"}
// 构建更新文档
update := bson.M{
"$set": bson.M{
"hobbies": bson.M{
"cities": []string{"shenzhen", "New York"},
"sports": []string{"skiing", "mountaineering"},
},
},
}
// 执行updateOne操作
result, _ := collection.UpdateOne(context.TODO(), filter, update)
fmt.Printf("Matched %v documents and updated %v documents.\n", result.MatchedCount, result.ModifiedCount)
|
示例2:查询所有 hobbies.cities 数组中包含 “shenzhen” 的文档
1
2
3
4
5
6
7
8
|
filter := bson.M{"hobbies.cities": "shenzhen"}
cursor, err := collection.Find(context.TODO(), filter)
defer cursor.Close(context.TODO())
for cursor.Next(context.TODO()) {
var stu Student
cursor.Decode(&stu)
fmt.Println(stu)
}
|
示例3:将 Alice 喜欢的城市添加一个 “chengdu”
1
2
3
4
5
6
7
8
|
filter := bson.M{"name": "Alice"}
update := bson.M{
"$push": bson.M{
"hobbies.cities": "chengdu",
},
}
// 执行updateOne操作
collection.UpdateOne(context.TODO(), filter, update)
|
执行多次会多次添加。
示例4:如果 Alice 喜欢的城市没有 “chengdu”,则添加该城市,否则什么也不做
1
2
3
4
5
6
7
8
|
filter := bson.M{"name": "Alice"}
update := bson.M{
"$addToSet": bson.M{
"hobbies.cities": "chengdu",
},
}
// 执行updateOne操作
collection.UpdateOne(context.TODO(), filter, update)
|
执行多次也只会添加一次。
示例5:从 Alice 的 hobbies.cities 数组中删除刚才添加的 “chengdu”
1
2
3
4
5
6
7
|
filter := bson.M{"name": "Alice"}
update := bson.M{
"$pull": bson.M{
"hobbies.cities": "chengdu",
},
}
collection.UpdateOne(context.TODO(), filter, update)
|
这个 updateOne
操作会找到 name
为 “Alice” 的第一个文档,并从其 hobbies.cities
数组中移除所有值为 “chengdu” 的元素。