Go 如何操作 MongoDB?
简介
MongoDB 是 NoSQL 中的一种。所谓的 NoSQL, 是指 Not Only SQL,不仅仅是 SQL。
MongoDB 具有一下基本特性:
- 面向集合存储:一个集合我们可以理解为 MySQL 中的表,集合中存储了很多文档,一个文档类似于 MySQL 中的一行数据。
- 模式自由:MongoDB 采用无模式结构存储数据,也就是说我们不需要预先定义文档必须满足的格式。
- 支持分片:MongoDB 支持分片,并且 MongoDB 自动解决了分片的各种问题,包括自动扩容。
- 强一致性:MongoDB 支持复制集,数据可以自动复制到多个节点,提供高可用性和数据冗余。当主节点故障时,可以自动切换到从节点,确保系统的可靠性。
MongoDB 的基本概念:
- 数据库(Database):MongoDB 中的数据库是文档集合的容器。一个 MongoDB 实例可以包含多个数据库,每个数据库有独立的权限控制。
- 集合(Collection):集合是文档的容器,类似于关系型数据库中的表。集合中的文档可以有不同的结构。
- 文档(Document):文档是 MongoDB 中的基本数据单元,使用 BSON 格式存储。每个文档是一个键值对的集合。
- 字段(Field):字段是文档中的键值对,类似于关系型数据库中的列。
安装
go get "go.mongodb.org/mongo-driver/mongo"
BSON
在 MongoDB 中,我们使用 BSON 定义结构体,你可以将它理解为类似于 JSON 一样的序列化与反序列化协议。MongoDB 在存储的时候,就是利用 BSON 将一个结构体转化为字节流,而后存储下来的。
在 BSON 中存在 EDMA类型,我们可以按照如下的理解方式:
- E:一个普通的键值对结构体,Value 可以是其他三个。
// E represents a BSON element for a D. It is usually used inside a D.
type E struct {
Key string
Value interface{}
}
- D:本质上是一个 E 的切片。
// D is an ordered representation of a BSON document. This type should be used when the order of the elements matters,
// such as MongoDB command documents. If the order of the elements does not matter, an M should be used instead.
//
// Example usage:
//
// bson.D{{"foo", "bar"}, {"hello", "world"}, {"pi", 3.14159}}
type D []E
- M:本质上是一个 Map,key 必须是
string
,value
可以是任何值,也可以是其他三种。
// M is an unordered representation of a BSON document. This type should be used when the order of the elements does not
// matter. This type is handled as a regular map[string]interface{} when encoding and decoding. Elements will be
// serialized in an undefined, random order. If the order of the elements matters, a D should be used instead.
//
// Example usage:
//
// bson.M{"foo": "bar", "hello": "world", "pi": 3.14159}
type M map[string]interface{}
- A:切片,元素可以是其他三个。
// An A is an ordered representation of a BSON array.
//
// Example usage:
//
// bson.A{"bar", "world", 3.14159, bson.D{{"qux", 12345}}}
type A []interface{}
初始化客户端
在初始化可以客户端时,我们一般分为三个步骤:
- 初始化客户端,注意传入的 option 里面传入了具体的链接信息。
- 获取一个 DataBase。
- 在 DataBase 里面获取一个集合。
func TestMogoDB(t *testing.T) {
// 控制初始化超时时间
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 首先初始化一个mongoDB客户端
opts := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.Connect(ctx, opts)
assert.NoError(t, err)
// 创建一个mongoDB的数据库
mdb := client.Database("we_book")
// 创建一个mongoDB的集合
col := mdb.Collection("article")
defer func() {
_, err = col.DeleteMany(ctx, bson.D{})
}()
}
定义字段
type Article struct {
Id int64 `bson:"id,omitempty"`
Title string `bson:"title,omitempty"`
Content string `bson:"content,omitempty"`
AuthorId int64 `bson:"author_id,omitempty"`
Status uint8 `bson:"status,omitempty"`
Ctime int64 `bson:"ctime,omitempty"`
Utime int64 `bson:"utime,omitempty"`
}
插入文档
插入文档与 ORM 类似,唯一的却别就是,MongoDB 没有自增主键的说法。
res, err := col.InsertOne(ctx, Article{
Id: 1,
Title: "我的标题",
Content: "我的内容",
AuthorId: 12,
Status: 1,
Ctime: time.Now().UnixMilli(),
Utime: time.Now().UnixMilli(),
})
if err != nil {
panic(err)
}
// InsertedID 返回插入的文档id,并非是article的id
fmt.Printf("插入结果的ID:%d\n", res.InsertedID)
// 插入结果的ID:[102 172 60 161 155 169 96 179 110 143 120 175]
查找文档
查找文档时候,我们可以使用 MongoDB 自己设计的查询方式,但是这种方式比较难用。我们可以用结构体自然构造生成查询条件,但要小心零值的处理。
// 查找 Id = 1 的文章
filter := bson.D{bson.E{Key: "id", Value: 1}}
var art Article
err = col.FindOne(ctx, filter).Decode(&art)
assert.NoError(t, err)
fmt.Printf("查找结果:%+v\n", art)
//查找结果:{Id:1 Title:我的标题 Content:我的内容 AuthorId:12 Status:1 Ctime:1722564149250 Utime:1722564149250}
使用结构体进行查询:
art = Article{}
err = col.FindOne(ctx, Article{Id: 1}).Decode(&art)
if err == mongo.ErrNoDocuments {
fmt.Println("没有数据")
}
//assert.NoError(t, err)
fmt.Printf("%#v \n", art)
更新文档
更新文档核心在两个点:
- 构造更新条件,和查找中的用法一模一样
- 构造更新字段:
- 利用 BSON 来更新
- 直接使用结构体本身,缺点是一样要忽略零值。
// 更新文档
sets := bson.D{bson.E{Key: "$set", Value: bson.E{Key: "title", Value: "新的标题"}}}
updateRes, err := col.UpdateOne(ctx, filter, sets)
assert.NoError(t, err)
fmt.Printf("更新后的文档数量:%d\n", updateRes.ModifiedCount)
// 更新后的文档数量:1
updateRes1, err := col.UpdateOne(ctx, filter, bson.D{
bson.E{Key: "$set", Value: Article{Title: "新的标题2", AuthorId: 123456}}})
assert.NoError(t, err)
fmt.Printf("更新后的文档数量:%d\n", updateRes1.ModifiedCount)
// 更新后的文档数量:1
删除文档
删除文档的核心在于构造删除条件,这一点和查询条件也是一样。
// 删除文档
filter = bson.D{bson.E{Key: "id", Value: 1}}
delRes, err := col.DeleteOne(ctx, filter)
assert.NoError(t, err)
fmt.Printf("删除后的文档数量:%d\n", delRes.DeletedCount)
// 删除后的文档数量:1
复杂查询 OR
// 使用 Or 查询条件
or := bson.A{bson.M{"id": 1}, bson.M{"id": 123}}
orRes, err := col.Find(ctx, bson.D{bson.E{"$or", or}})
assert.NoError(t, err)
var arts []Article
err = orRes.All(ctx, &arts)
assert.NoError(t, err)
fmt.Printf("使用or进行查找结果:%+v\n", arts)
// 使用or进行查找结果:[{Id:1 Title:新的标题2 Content:我的内容 AuthorId:123456 Status:1 Ctime:1722576620900 Utime:1722576620900}]
复杂查询 AND
// 使用 And 查询条件
and := bson.A{bson.D{bson.E{"id", 1}},
bson.D{bson.E{"title", "新的标题2"}}}
andRes, err := col.Find(ctx, bson.D{bson.E{"$and", and}})
assert.NoError(t, err)
ars := []Article{}
err = andRes.All(ctx, &ars)
assert.NoError(t, err)
fmt.Printf("使用and进行查找结果:%+v\n", ars)
// 使用and进行查找结果:[{Id:1 Title:新的标题2 Content:我的内容 AuthorId:123456 Status:1 Ctime:1722576444390 Utime:1722576444390}]
复杂查询 IN
// IN 查询
in := bson.D{bson.E{Key: "id", Value: bson.M{"$in": []any{1, 2, 3}}}}
inRes, err := col.Find(ctx, in)
ars = []Article{}
err = inRes.All(ctx, &ars)
assert.NoError(t, err)
fmt.Printf("使用in查找结果:%+v\n", ars)
// 使用in查找结果:[{Id:1 Title:新的标题2 Content:我的内容 AuthorId:123456 Status:1 Ctime:1722576444390 Utime:1722576444390}]
复杂查询 特定字段查询
inRes, err = col.Find(ctx, in, options.Find().SetProjection(bson.M{
"id": 1,
"title": "我的标题",
}))
ars = []Article{}
err = inRes.All(ctx, &ars)
assert.NoError(t, err)
fmt.Printf("自定义查询的查找结果:%+v\n", ars)
// 自定义查询的查找结果:[{Id:1 Title:我的标题 Content: AuthorId:0 Status:0 Ctime:0 Utime:0}]
索引
类似于 MySQL,一般都是根据业务中最常用的那些查询条件,来决定在什么列上创建索引。
idxRes, err := col.Indexes().CreateMany(ctx, []mongo.IndexModel{
{
Keys: bson.M{"id": 1},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.M{"author_id": 1},
},
})
assert.NoError(t, err)
fmt.Printf("创建索引结果:%+v\n", idxRes)
// 创建索引结果:[id_1 author_id_1]
注意
在使用时,一定会要注意对零值的判断,否则可能会出现无法查不到数据的问题。