为了演示如何使用 Golang 结构体和 MongoDB,我创建了一个简单的电子邮件自动回复器作为示例。我们有一些联系人,我们希望向他们发送电子邮件。此外,我们有要在特定时间发送的电子邮件,带有主题和内容。最后,我们还有一个将联系人与电子邮件连接起来的序列。
通过使用 Golang 结构体,我们可以最小化代码中对 BSON 的使用,并增加来自数据库查询结果的可用性。本文可以视为之前一篇文章的续篇,该文章名为 “如何使用 Golang 和 MongoDB.”
接下来,我将逐个解释演示代码的每个部分。
导入相关包
首先,我们将导入必要的 Golang 包。
package main
import (
"context"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"time"
)
我们需要导入这些库来执行以下操作:
context
是必要的,用于 创建 MongoDB 操作的上下文。bson
用于序列化发送到 MongoDB 服务器的数据。mongo
用于连接 MongoDB 服务器并设置数据库和集合。
创建结构体
与我们在 之前的文章 中使用 bson
序列化不同,这里的想法是使用 Golang 结构体。这些结构体需要使用 bson
注释才能正常工作。
type Contact struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Name string `bson:"name,omitempty"`
Email string `bson:"email,omitempty"`
Tags []string `bson:"tags,omitempty"`
}
type Email struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
SendDate time.Time `bson:"senddate,omitempty"`
Subject string `bson:"subject,omitempty"`
Content string `bson:"content,omitempty"`
}
type Sequence struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Emails []interface{} `bson:"emails,omitempty"`
Receivers []primitive.ObjectID `bson:"receivers,omitempty"`
}
在上面的代码片段中,我们可以看到各种 bson
注释。
这些注释都遵循相同的模式:
bson:"<fieldname>,omitempty"
其中<fieldname>
是数据库中实际字段名称,omitempty
表示如果没有给出值,则省略该字段。
请注意,注释也在 下降的重音符号 之间,并且 没有空格。
你可以使用其他结构标记(或根本不使用标记)代替 omitempty
。请在 此处 了解更多信息。
在第 2、9 和 16 行中,我们将 ID
字段的类型设置为 primitive.ObjectID
。通过这样做,我们告诉 BSON 这些字段将是实际的 MongoDB ObjectIDs
。
此外,即使我们在定义这些结构体的实例时省略了 _id
数据库字段,它们也将始终填充。MongoDB 服务器将填充这些字段并为它们提供唯一的 ObjectIDs
。我们将使用这些 ObjectIDs
来定位集合中的单个文档。
在第 5 行中,我们创建一个字符串切片。该切片将自动转换为 bson.A
(BSON 数组)。
在第 10 行中,你可以看到还可以使用 time.Time
字段。
在第 17-18 行中,我们将在数据库中放置对其他文档的引用。这些引用将是 ObjectIDs
的列表。在这里,我向你展示了两种方法。
你可以使用 []interface{}
,这是你可以使用的最通用的方法。危险是你可以在此处放置任何值-包括整数、字符串等。
另一种可能性是使用 []primitive.ObjectID
。这是一种声明类型的特定方法。现在只有 ObjectIDs
将被接受。这种方法的问题是我们稍后需要编写更多代码。
MongoDB 连接和数据库设置
在 main()
函数中要做的第一件事是连接到 MongoDB 服务器并创建包含文档的集合的数据库。
opt := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.NewClient(opt)
if err != nil {
panic(err)
}
ctx := context.TODO()
err = client.Connect(ctx)
if err != nil {
panic(err)
}
defer client.Disconnect(ctx)
autoresponderDB := client.Database("autoresponder")
contactsCollection := autoresponderDB.Collection("contacts")
emailsCollection := autoresponderDB.Collection("emails")
sequencesCollection := autoresponderDB.Collection("sequences")
defer contactsCollection.Drop(ctx)
defer emailsCollection.Drop(ctx)
defer sequencesCollection.Drop(ctx)
在第 1 和 2 行中,我们使用 NewClient()
和正确的 URI 创建了 MongoDB 的新客户端。请注意,我的 MongoDB 在本地主机上运行,端口为 27017
。未设置用户名或密码,因为这是我的测试服务器。
在第 7 行中,我们使用 context.TODO()
创建上下文。这是可能的最基本的上下文。
在第 9 行中,我们让客户端使用给定的上下文连接到 MongoDB 服务器。
在第 14 行中,我们 defer
断开与 MongoDB 服务器的连接。鉴于 Disconnect()
函数被推迟执行,它将在运行 main()
函数中的所有其他语句之后执行。
在第 16 行中,我们创建了一个名为 autoresponder
的数据库。
在第 17-19 行中,我们在此数据库中创建了三个集合。这些集合分别称为 contacts
、emails
和 sequences
。
在第 21-23 行中,我们 defer
删除数据库集合。这仅适用于此处的示例,因为我不希望每次运行代码并测试新内容时都填满数据库。如果你希望保留数据库中的数据,请删除这些行。
数据库插入
将联系人插入数据库
让我们创建一些模拟联系人并将它们插入 contactsCollection
。
manyContacts := []interface{}{
Contact{
Name: "John Doe",
Email: "john@doe.com",
Tags: []string{"Lead", "Subscriber"},
},
Contact{
Name: "Mia May",
Email: "mm@example.com",
Tags: []string{"Lead", "Customer"},
},
Contact{
Name: "Lia Shmia",
Email: "lia@shmia.com",
Tags: []string{"Lead", "Customer"},
},
}
insertResult, err := contactsCollection.InsertMany(ctx, manyContacts)
if err != nil {
panic(err)
}
contactIDs := insertResult.InsertedIDs
var contactIDs_ []primitive.ObjectID
for _, id := range contactIDs {
contactIDs_ = append(contactIDs_, id.(primitive.ObjectID))
}
fmt.Printf("Inserted %v %T\n", contactIDs_, contactIDs_)
在第 1-17 行中,我们创建了三个联系人(MongoDB 术语中的“文档”),方式与创建常规结构体相同。请注意,这与我们在 这里 使用 BSON 不同(请向下滚动到页面的中间)。
在第 19 行中,我们使用 InsertMany()
将这些联系人插入 contact
集合,因为我们希望一次插入多个文档。第24行,我们使用第19行的结果insertResult
和属性InsertedIDs
获取插入联系人的ID。
第26行,我们创建一个类型为[]primitive.ObjectID
的切片contactIDs_
。
在第17至29行,我们循环遍历返回的contactIDs
,将每个元素转换为primitive.ObjectID
并将其附加到contactIDs_
。显然,无法从[]interface{}
转换为[]primitive.ObjectID
;这就是为什么我们使用for
循环的原因。
请注意,我们没有自己定义ID。在插入文档时,MongoDB自动分配了ID。这些ID保存在实际数据库的_id
字段中,也可以使用ID
属性在结构中访问。
在第31行,我们打印出contactIDs_
切片,以证明我们插入了三个联系人并且类型是正确的。
我们还可以使用MongoDB Compass来检查此操作。
将电子邮件插入数据库
现在,让我们也将一些电子邮件插入到emailsCollection
中。
manyEmails := []interface{}{
Email{
SendDate: time.Now(),
Subject: "[Newsletter] Thank you!",
Content: "Thank you for subscribing to our weekly newsletter!"},
Email{
SendDate: time.Now(),
Subject: "[Newsletter] The first email",
Content: "In this email we'll discuss..."},
Email{
SendDate: time.Now(),
Subject: "[Newsletter] The second email",
Content: "In the previous email I mentioned that..."},
}
insertResult2, err := emailsCollection.InsertMany(ctx, manyEmails)
if err != nil {
panic(err)
}
emailIDs := insertResult2.InsertedIDs
fmt.Printf("Inserted %v %T\n", emailIDs, emailIDs)
这与插入联系人几乎相同。
要注意的是,在第3、7和11行,我们使用time.Now()
作为变量值。
在第21行,我们从第16行的InsertMany()
操作的返回结果中检索emailIDs
。
将序列插入数据库
最后,让我们也在sequencesCollection
中插入一个序列。
oneSequence := Sequence{Emails: emailIDs, Receivers: contactIDs_}
insertResult3, err := sequencesCollection.InsertOne(ctx, oneSequence)
if err != nil {
panic(err)
}
fmt.Println("Inserted", insertResult3.InsertedID, insertResult3.InsertedID)
在第1行,我们使用Sequence
结构创建一个序列。在此结构中,我们将从先前的插入结果中检索的emailIDs
和contactIDS_
放入其中。
在第3行,我们使用InsertOne()
而不是我们之前使用的InsertMany()
。
数据库查询
查找特定联系人
在下面的代码中,我演示了如何使用自定义过滤器查找联系人。
var contacts []Contact
contactCursor, err := contactsCollection.Find(ctx, bson.M{"tags": "Customer"})
if err != nil {
panic(err)
}
if err = contactCursor.All(ctx, &contacts); err != nil {
panic(err)
}
for _, c := range contacts {
fmt.Printf("%v of type %T\n", c.Name, c.Name)
fmt.Printf("%v of type %T\n", c.Email, c.Email)
fmt.Printf("%v of type %T\n", c.Tags, c.Tags)
}
在第2行,我们使用Find()
函数查找满足过滤器bson.M{"tags": "Customer"}
的所有contacts
。使用此过滤器,我们将找到所有在其标记列表中具有"Customer"
的联系人。
当然,你可以根据自己的需要调整此过滤器。如果要查找所有联系人,则可以使用bson.M{}
。或者,如果你希望找到将mm@example.com
作为电子邮件地址的联系人,则可以编写bson.M{"email":"mm@example.com"}
。
此外,你可以使用bson.D
格式而不是使用bson.M
格式作为过滤器。更多信息请参见此处。
在第7行,查询结果使用All()
加载到名为contacts
的[]Contact
切片中。
在第11至15行,我们只是打印出一些Contact
结构的属性,就像我们处理任何结构一样。
这样,我们可以最大限度地利用Golang结构并减少对BSON的使用来过滤查询。
查找序列并检索联系人和电子邮件
以下,我们从sequencesCollection
中恢复序列。我们从此序列中获取电子邮件ID和接收者的ID。然后,使用这些ID检索相应的电子邮件和联系人。
var sequences []Sequence
sequenceCursor, err := sequencesCollection.Find(ctx, bson.M{})
if err != nil {
panic(err)
}
if err = sequenceCursor.All(ctx, &sequences); err != nil {
panic(err)
}
fmt.Println(sequences[0].ID)
emailSequence := sequences[0].Emails
receivers := sequences[0].Receivers
for ix, e := range emailSequence {
var result Email
err := emailsCollection.FindOne(ctx, bson.M{"_id": e}).Decode(&result)
if err != nil {
panic(err)
}
for _, e2 := range receivers {
var result2 Contact
err := contactsCollection.FindOne(ctx, bson.M{"_id": e2}).Decode(&result2)
if err != nil {
panic(err)
}
fmt.Printf("Sending email %v with subject %v to %v\n", ix, result.Subject, result2.Email)
}
}
在第10至13行中,我们使用[0]
,因为只返回了一个序列。
在第17行和第23行中,我们使用FindOne()
与bson.M{"_id": <id>}
一起,仅基于其ObjectID
(在<id>
中)查找并过滤电子邮件和联系人中的一个特定文档。由于只有一个结果,我们也可以使用Decode()
而不是All()
将结果提取到Email
和Contact
结构中。
完整的代码
下面是完整的代码。在执行代码之前,请确保在mongodb://localhost:27017
上运行MongoDB服务器-或更改URI。
package main
import (
"context"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"time"
)
type Contact struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Name string `bson:"name,omitempty"`
Email string `bson:"email,omitempty"`
Tags []string `bson:"tags,omitempty"`
}
type Email struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
SendDate time.Time `bson:"senddate,omitempty"`
Subject string `bson:"subject,omitempty"`
Content string `bson:"content,omitempty"`
}
type Sequence struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Emails []interface{} `bson:"emails,omitempty"`
Receivers []primitive.ObjectID `bson:"receivers,omitempty"`
}
func main() {
opt := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.NewClient(opt)
if err != nil {
panic(err)
}
ctx := context.TODO()
err = client.Connect(ctx)
if err != nil {
panic(err)
}
defer client.Disconnect(ctx)
autoresponderDB := client.Database("autoresponder")
contactsCollection := autoresponderDB.Collection("contacts")
emailsCollection := autoresponderDB.Collection("emails")
sequencesCollection := autoresponderDB.Collection("sequences")
defer contactsCollection.Drop(ctx)
defer emailsCollection.Drop(ctx)
defer sequencesCollection.Drop(ctx)
manyContacts := []interface{}{
Contact{
Name: "John Doe",
Email: "john@doe.com",
Tags: []string{"Lead", "Subscriber"},
},
Contact{
Name: "Mia May",
Email: "mm@example.com",
Tags: []string{"Lead", "Customer"},
},
Contact{
Name: "Lia Shmia",
Email: "lia@shmia.com",
Tags: []string{"Lead", "Customer"},
},
}
insertResult, err := contactsCollection.InsertMany(ctx, manyContacts)
if err != nil {
panic(err)
}
contactIDs := insertResult.InsertedIDs
var contactIDs_ []primitive.ObjectID
for _, id := range contactIDs {
contactIDs_ = append(contactIDs_, id.(primitive.ObjectID))
}
fmt.Printf("Inserted %v %T\n", contactIDs_, contactIDs_)
manyEmails := []interface{}{
Email{
SendDate: time.Now(),
Subject: "[Newsletter] Thank you!",
Content: "Thank you for subscribing to our weekly newsletter!"},
Email{
SendDate: time.Now(),
Subject: "[Newsletter] The first email",
Content: "In this email we'll discuss..."},
Email{
SendDate: time.Now(),
Subject: "[Newsletter] The second email",
Content: "In the previous email I mentioned that..."},
}
insertResult2, err := emailsCollection.InsertMany(ctx, manyEmails)
if err != nil {
panic(err)
}
emailIDs := insertResult2.InsertedIDs
fmt.Printf("Inserted %v %T\n", emailIDs, emailIDs)
oneSequence := Sequence{Emails: emailIDs, Receivers: contactIDs_}
insertResult3, err := sequencesCollection.InsertOne(ctx, oneSequence)
if err != nil {
panic(err)
}
fmt.Println("Inserted", insertResult3.InsertedID, insertResult3.InsertedID)
var contacts []Contact
contactCursor, err := contactsCollection.Find(ctx, bson.M{"tags": "Customer"})
if err != nil {
panic(err)
}
if err = contactCursor.All(ctx, &contacts); err != nil {
panic(err)
}
for _, c := range contacts {
fmt.Printf("%v of type %T\n", c.Name, c.Name)
fmt.Printf("%v of type %T\n", c.Email, c.Email)
fmt.Printf("%v of type %T\n", c.Tags, c.Tags)
}
var sequences []Sequence
sequenceCursor, err := sequencesCollection.Find(ctx, bson.M{})
if err != nil {
panic(err)
}
if err = sequenceCursor.All(ctx, &sequences); err != nil {
panic(err)
}
fmt.Println(sequences[0].ID)
emailSequence := sequences[0].Emails
receivers := sequences[0].Receivers
for ix, e := range emailSequence {
var result Email
err := emailsCollection.FindOne(ctx, bson.M{"_id": e}).Decode(&result)
if err != nil {
panic(err)
}
for _, e2 := range receivers {
var result2 Contact
err := contactsCollection.FindOne(ctx, bson.M{"_id": e2}).Decode(&result2)
if err != nil {
panic(err)
}
fmt.Printf("Sending email %v with subject %v to %v\n", ix, result.Subject, result2.Email)
}
}
// time.Sleep(40 * time.Second)
}
参考资料
MongoDB发布的“Quick Start: Golang & MongoDB — Modeling Documents with Go Data Structures”
MongoDB文档中关于“Work with BSON”的说明
StackOverflow上关于“bson.D vs bson.M for find queries”的问题
译自:https://betterprogramming.pub/how-to-use-golang-structs-with-mongodb-f1772e4a1da3
评论(0)