Golang 操作MongoDB
MongoDB
MongoDB属于非关系型数据库,是由C++编写的分布式文档数据库。内部使用类似于Json的bson二进制格式。
中文手册
https://www.w3cschool.cn/mongodb/
安装
https://www.mongodb.com/try/download/community
自行下载对应操作系统的MongoDB,并运行它。
windows可以下载官方zip,解压即可使用。
组件 | 文件名 |
---|---|
Server | mongod.exe |
Router | mongos.exe,Query Router,Sharding Cluster |
Client | mongo.exe |
MonitoringTools | mongostat.exe,mongotop.exe |
ImportExportTools | mongostat.exe,mongotop.exe |
MiscellaneousTools | bsondump.exe,mongofiles.exe,mongooplog.exe,mongoperf.exe |
运行
$ cd /o/mongodb3.6/bin
$ ./mongod.exe
2019-08-02T03:26:13.234-0700 I STORAGE [initandlisten] exception ininitAndListen: NonExistentPath: Data directory O:\data\db\ not found., terminating
启动服务出错,原因在于找不到数据目录。默认是/data/db
windows下在当前盘符根目录下创建目录即可`o:/data/db`
选项说明
- --bind_ip ip逗号分隔IP地址。默认为localhost
- --bind_ip_all 绑定所有本地ip地址
- --port port端口,默认27017
- --dbpath 数据路径,却省委/data/db
- --logpath 指定日志路径,代替stdout,默认是控制台打印日志
- -f file 指定配置文件,yaml格式
- 注册windows服务
- --install 注册windows服务
- --serviceName name服务名称
- --serviceDisplayName name 服务显示名
配置文件
mongodb配置使用Yaml格式
- 嵌套使用缩进完成,不支持Tab等制表符,支持空格
- 缩进空格数不限制,只要同一级元素对齐就行
- 冒号后要有空格
- 大小写敏感
- #表示注释
- 字符串不需要引号,有特殊字符串时可以使用引号
- 布尔
- true、True、TRUE、yes、YES都是真
- false、False、FALSE、no、NO都是假
- null、Null、~波浪线都是空,不指定值默认也是空
Yaml参考 https://www.w3cschool.cn/iqmrhf/dotvpozt.html
配置 http://mongoing.com/docs/reference/configuration-options.html
systemLog:
destination: file
path: 'o:/mongodb3.6/logs/mongod.log'
logAppend: true
storage:
dbPath: "o:/mongodb3.6/db"
net:
bindIp: "127.0.0.1"
port: 27017
选项
- systemLog
- destination,缺省是输出日志到std,file表示输出到文件
- path,日志文件路径。文件目录必须存在
- logAppend,true表示在已存在的日志文件追加。默认为false,每次启动服务创建新的日志
- storage
- dbPath,必须指定mongodb的数据目录,目录必须存在
- net
- bindIp ,缺省绑定到127.0.0.1
- port 端口,缺省27017,客户端连接用的
Windows下注册为服务的命令如下,使用了配置文件:
$ mongod.exe -f "o:/mongodb3.6/bin/mongod.yml" --serviceName mongod --serviceDisplayName mongo --install
注意,注册服务得需要管理员权限。
storage:
dbPath: "o:/mongodb3.6/db"
net:
bindIp: "127.0.0.1"
port: 27017
没有配置日志,信息将显示在控制台中
$ pwd
/o/mongodb3.6
$ mongod.exe -f ./mongod.yml
客户端
客户端连接
$ bin/mongo.exe
MongoDB shell version v3.6.13
help 打开帮助
show dbs 查看当前有哪些库
use blog 有就切换过去,没有就创建后切换过去。刚创建的并不在数据库列表中,需要写入数据后才能看到
db 查看当前数据库
db.users.insert({user:"tom", age:20}) db指代当前数据库;users集合名
也可以使用官方的可视化工具Compass。https://www.mongodb.com/products/compass
驱动
驱动链接:https://www.mongodb.com/docs/drivers/
Go驱动: https://www.mongodb.com/docs/drivers/go/current/
驱动安装:
$ go get go.mongodb.org/mongo-driver/mongo
连接字符串
https://www.mongodb.com/docs/manual/reference/connection-string/#examples
mongodb://[username:password@]host1[:port1][,...hostN[:portN]]
[/[defaultauthdb][?options]]
mongodb://wayne:wayne@mongodb0.example.com:27017
链接例子https://www.mongodb.com/docs/drivers/go/current/fundamentals/connection/#connection-example
package main
import (
"context"
"fmt"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Replace the placeholder with your Atlas connection string
const uri = "<connection string>"
func main() {
// Use the SetServerAPIOptions() method to set the Stable API version to 1
serverAPI := options.ServerAPI(options.ServerAPIVersion1)
opts := options.Client().ApplyURI(uri).SetServerAPIOptions(serverAPI)
// Create a new client and connect to the server
client, err := mongo.Connect(context.TODO(), opts)
if err != nil {
panic(err)
}
defer func() {
if err = client.Disconnect(context.TODO()); err != nil {
panic(err)
}
}()
// Send a ping to confirm a successful connection
var result bson.M
if err := client.Database("admin").RunCommand(context.TODO(), bson.D{{"ping", 1}}).Decode(&result); err != nil {
panic(err)
}
fmt.Println("Pinged your deployment. You successfully connected to MongoDB!")
}
连接副本集
mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myRS
账号密码认证连接
官网:https://www.mongodb.com/docs/drivers/go/current/fundamentals/auth/
支持的机制
Go驱动程序支持以下身份验证机制
Go Driver通过身份验证机制建立连接客户类型。该Client类型指定用作连接选项的机制和凭据。凭据类型。要配置这些选项,请将Credential类型传递给设置验证() 的方法客户选项类型。
约定示例
每个身份验证机制都包括以下占位符:
username
mongodb用户名password
mongodb密码hostname
mongodb服务器ip地址port
端口authenticationDb
包含用户身份验证数据的mongodb数据库。如果省略此选项,驱动程序将使用默认值admin
默认方式
默认机制使用以下身份验证机制之一,具体取决于服务器支持的mongodb版本
机制 | 版本 |
---|---|
SCRAM-SHA-256 |
Mongodb4.0及更高版本 |
SCRM-SHA-1 |
MongoDB3.0、3.2、3.4和3.6 |
MONGODB-CR |
MongoDB及更早版本 |
credential := options.Credential{
AuthSource: "<authenticationDb>",
Username: "<username>",
Password: "<password>",
}
clientOpts := options.Client().ApplyURI("mongodb://<hostname>:<port>").
SetAuth(credential)
client, err := mongo.Connect(context.TODO(), clientOpts)
SCRAM-SHA-256
SCRAM-SHA-256****是从 MongoDB 4.0 开始的 MongoDB 默认身份验证方法。
SCRAM-SHA-256
是一种加盐质询-响应身份验证机制 (SCRAM),它使用您的用户名和密码(通过算法加密)SHA-256
来对您的用户进行身份验证。
要指定SCRAM-SHA-256
身份验证机制,请为 AuthMechanism
选项分配值"SCRAM-SHA-256"
:
credential := options.Credential{
AuthMechanism: "SCRAM-SHA-256",
AuthSource: "<authenticationDb>",
Username: "<username>",
Password: "<password>",
}
clientOpts := options.Client().ApplyURI("mongodb://<hostname>:<port>").
SetAuth(credential)
client, err := mongo.Connect(context.TODO(), clientOpts)
SCRAM-SHA-1
SCRAM-SHA-1****是 MongoDB 版本 3.0、3.2、3.4 和 3.6 的默认身份验证方法。
SCRAM-SHA-1
是一种加盐质询响应机制 (SCRAM),它使用您的用户名和密码(通过算法加密)SHA-1
来验证您的用户。
要指定SCRAM-SHA-1
身份验证机制,请为 AuthMechanism
选项分配值"SCRAM-SHA-1"
:
credential := options.Credential{
AuthMechanism: "SCRAM-SHA-1",
AuthSource: "<authenticationDb>",
Username: "<username>",
Password: "<password>",
}
clientOpts := options.Client().ApplyURI("mongodb://<hostname>:<port>").
SetAuth(credential)
client, err := mongo.Connect(context.TODO(), clientOpts)
连接测试
package main
import (
"context" // 用于在函数之间传递上下文信息,以支持控制连接和请求的声明周期
"fmt"
"go.mongodb.org/mongo-driver/bson" // 用于处理mongodb的bson数据
"go.mongodb.org/mongo-driver/bson/primitive" // 用于处理mongodb的objectid类型
"go.mongodb.org/mongo-driver/mongo" // mongodb官方go驱动
"go.mongodb.org/mongo-driver/mongo/options" // 用于设置mongodb连接选项
"time"
)
var client *mongo.Client // 定义客户端连接
var database *mongo.Database // 数据库实例
var users *mongo.Collection // 集合实例
func init() {
//const uri = "mongodb://dujie:123456@localhost:27017"
// mongodb连接字符串
const uri = "mongodb://localhost:27017"
// 创建凭据对象,用于身份验证
credential := options.Credential{
AuthSource: "test",
Username: "dujie",
Password: "123456",
}
// options.Client().ApplyURI(uri) 创建一个连接选项,SetAuth(credential) 设置身份验证, SetConnectTimeout设置连接超时时间
opts := options.Client().ApplyURI(uri).SetAuth(credential).SetConnectTimeout(time.Second * 3)
//opts := options.Client().ApplyURI(uri).SetConnectTimeout(time.Second * 3)
var err error
// mongo.Connect 连接到mongodb数据库,返回客户端实例
client, err = mongo.Connect(context.TODO(), opts)
if err != nil {
fmt.Println("1 ~~~~", err)
}
// 测试与数据库的连接是否正常
err = client.Ping(context.TODO(), nil)
if err != nil {
fmt.Println("ping ~~~~", err)
}
// 获取数据库实例
database = client.Database("test")
// 获取集合(表)实例
users = database.Collection("users")
fmt.Println(database)
fmt.Println(users)
}
type User struct {
// `bson:"_id,omitempty" 表示在BSON序列化和反序列化时将该字段映射到数据库中的_id字段,
Id primitive.ObjectID `bson:"_id,omitempty"`
Name string
Age int
Score float64
}
func main() {
var u User
err := users.FindOne(context.TODO(), bson.M{}).Decode(&u)
if err != nil {
fmt.Println(err)
}
fmt.Println(u)
defer func() {
err := client.Disconnect(context.TODO())
if err != nil {
fmt.Println("2 ~~~~", err)
}
}()
}
基本概念
MongoDB中可以创建使用多个库,但是有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库。
admin:从权限的角度来看,这是”root”数据库。要是将一个用户添加到这个数据库中,这个用户自动继承所有数据库的权限,一些特定的服务器端命令也只能从这个数据库运行,比如列出所有数据库或者关闭服务器。
admin
数据库是MongoDB的管理数据库,只有具有管理员权限的用户才能访问它。- 在
admin
数据库中,可以进行用户管理、权限设置、数据库备份和还原等操作。 - 通常,管理员会在
admin
数据库下创建其他数据库的用户,并为这些用户分配相应的权限。
local:这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合。
local
数据库也是与分片相关的数据库,但它只在每个分片节点上存在。local
数据库用于存储分片节点的复制集信息和本地操作日志。- 当一个节点作为分片副本集的成员时,它会在
local
数据库中记录与副本集相关的信息。
config:当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
config
数据库是用于支持MongoDB的分片(Sharding)功能的配置数据库。- 当MongoDB部署为分片集群时,
config
数据库中会存储与分片有关的配置信息,如分片键、分片节点等。 - 分片配置服务器会维护
config
数据库中的信息,用于管理数据的分片和迁移。
RDBMS | MongoDB |
---|---|
Database | Database |
Table | Collection |
Row | Document |
Column | Field |
Join | Embedded Document嵌入文档或Reference引入 |
Primary Key | 主键(MongoDB提供了key为_id) |
Go Driver使用,官方博客:https://www.mongodb.com/blog/post/mongodb-go-driver-tutorial
数据封装
结构体定义https://www.mongodb.com/docs/drivers/go/current/usage-examples/findOne/#find-a-document
type Restaurant struct {
ID primitive.ObjectID `bson:"_id"`
Name string
RestaurantId string `bson:"restaurant_id"`
Cuisine string
Address interface{}
Borough string
Grades []interface{}
}
type User struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Name string
Age int
}
func (u User) String() string {
return fmt.Sprintf("<%s: %s,%d>", u.ID, u.Name, u.Age) }
结构体Tag 参考: https://www.mongodb.com/docs/drivers/go/current/fundamentals/bson/#struct-tags
结构体标签 | 描述 |
---|---|
omitempty |
如果将字段设置为与字段类型对应的零值,则不会对字段进行编辑,一般用于设置_id |
minsize |
如果字段类型为int64 、uint32 或uint64 类型,并且该字段的值可以放入有符号int32 中,则该字段将被序列化为BSON int32 ,而不是BSON int64 。如果该值无法容纳有符号的int32 则忽略此标记 |
truncate |
如果字段类型是非浮点数字类型,则解组到该字段的BSON双精度将在小数点处被截断。 |
inline |
如果字段类型是结构体或映射字段,则该字段在编组时展平,并在解组时取消展平。 |
User结构体中ID一定要使用omitempty,新增结构体时,如果ID不设置则为零值提交,数据库中_id字段就是一串0。如果设置忽略零值,ID为0提交时会被忽略,数据库则自动生成_id中的id。
ObjectId有12字节组成,参考bson/primitive/objectid.go/NewObjectID()函数
- 4字节时间戳
- 5字节进程唯一值
- 3字节随机数,每次加1
插入数据
操作参考:https://www.mongodb.com/docs/drivers/go/current/usage-examples/
####### 插入单条文档
type Restaurant struct {
Name string
RestaurantId string `bson:"restaurant_id,omitempty"`
Cuisine string `bson:"cuisine,omitempty"`
Address interface{} `bson:"address,omitempty"`
Borough string `bson:"borough,omitempty"`
Grades []interface{} `bson:"grades,omitempty"`
}
当结构omitempty
标记留空时,它会缺省插入文档中的相应字段。
以下示例将新文档插入到restaurants
集合中
不存在的数据库和集合
如果执行写入操作时所需的数据库和集合不存在,服务器会隐式创建它们
coll := client.Database("sample_restaurants").Collection("restaurants")
newRestaurant := Restaurant{Name: "8282", Cuisine: "Korean"}
result, err := coll.InsertOne(context.TODO(), newRestaurant)
if err != nil {
panic(err)
}
结果:
{
"_id": ObjectId("..."),
"name": "8282",
"cuisine": "Korean"
}
####### 插入多条
可以使用该InsertMany()
方法将多个文档插入到集合中。
type Restaurant struct {
Name string
RestaurantId string `bson:"restaurant_id,omitempty"`
Cuisine string `bson:"cuisine,omitempty"`
Address interface{} `bson:"address,omitempty"`
Borough string `bson:"borough,omitempty"`
Grades []interface{} `bson:"grades,omitempty"`
}
当结构omitempty
标记留空时,它会省略插入文档中的相应字段。
以下示例将两个新文档插入到restaurants
集合中:
coll := client.Database("sample_restaurants").Collection("restaurants")
newRestaurants := []interface{}{
Restaurant{Name: "Rule of Thirds", Cuisine: "Japanese"},
Restaurant{Name: "Madame Vo", Cuisine: "Vietnamese"},
}
result, err := coll.InsertMany(context.TODO(), newRestaurants)
if err != nil {
panic(err)
}
BSON
https://www.mongodb.com/docs/drivers/go/current/fundamentals/bson/
将Go类型转换为BSON的过程称为编组,而相反的过程称为解组
MongoDB的Go库提供的构建BSON的数据分为4种
MongoDB以二进制表示形式存储文档,称为BSON允许轻松灵活的数据处理。
- D : An ordered representation of a BSON document (slice),表示有序的,切片且元素是二元的
- M : An unordered representation of a BSON document (map),表示无序的,map且元素是kv对
- A : An ordered representation of a BSON array BSON数组的有序表示
- E : A single element inside a D type D类型内的单个元素
以下示例演示如何使用 bson.D
类型构造查询过滤器来匹配quantity
字段值大于 100 的文档
filter := bson.D{{"quantity", bson.D{{"$gt", 100}}}}
查询
####### 单条查询bson.D{{"name","tom"}}
- bson.D 是切片,D后的{}表示切片字面量定义
- {“name”,”tom”} 表示一个结构体实例字面量定义
- “name” 是结构体的Key属性,类型是string
- “tom”是结构体的value属性,类型是any
bson.M{"name":"tom"}
- bson.M是map,M后的{} 表示该map的字面量定义
- map类型为map[string]interface{}
// 找一条
func findOne() {
// 条件
// filter := bson.D{{"name", "tom"}} // slice
// filter := bson.D{{"name", bson.D{{"$eq", "tom"}}}}
filter := bson.M{"name": "tom"} // map
// filter := bson.M{"name": bson.M{"$ne": "jerry"}}
// filter := bson.D{} // 没有条件全部都符合
var u User
err := users.FindOne(context.TODO(), filter).Decode(&u)
if err != nil {
if err == mongo.ErrNoDocuments {
// 说明没有任何匹配文档
log.Println("没有匹配的文档")
return
}
log.Fatal(err)
}
fmt.Println(u) }
####### 多条查询
// 查多条,遍历结果
func findMany1() {
filter := bson.M{} // 无条件,全部符合
cursor, err := users.Find(context.TODO(), filter)
if err != nil {
log.Fatal(err)
}
defer cursor.Close(context.TODO()) // 关闭游标
var results []*User
for cursor.Next(context.TODO()) {
var u User
err = cursor.Decode(&u)
if err != nil {
log.Fatal(err)
}
results = append(results, &u) // 装入容器
}
fmt.Println(results) }
// 查多条,成批装入容器
func findMany2() {
filter := bson.D{} // 无条件,全部符合
var results []*User
cursor, err := users.Find(context.TODO(), filter)
if err != nil {
log.Fatal(err)
}
defer cursor.Close(context.TODO()) // 关闭游标
err = cursor.All(context.TODO(), &results)
if err != nil {
log.Fatal(err)
}
for i, r := range results {
fmt.Println(i, r)
}
}
查询条件
改造上面的findMany2函数,可以使用下面表格中不同的filter
func findByFilter(filter interface{}) {
var results []*User
cursor, err := users.Find(context.TODO(), filter)
if err != nil {
log.Fatal(err)
}
defer cursor.Close(context.TODO()) // 关闭游标
err = cursor.All(context.TODO(), &results)
if err != nil {
log.Fatal(err)
}
fmt.Println(results) }
比较符号 | 含义 | filter示例 |
---|---|---|
$gt | 大于 | bson.M{"age":bson.M{"$gt":32}} |
$lt | 小于 | bson.M{"age":bson.M{"$lt":32}} |
$lte | 小于等于 | bson.M{"age":bson.M{"$lte":32}} |
$gte | 大于等于 | bson.M{"age":bson.M{"$gte":32}} |
$ne | 不等于 | bson.M{"age":bson.M{"$ne":32}} |
$eq | 等于,可以不用这个符号 | bson.M{"age": bson.M{"$eq": 20}} bson.M{"age": 20} |
$in | 在范围内 | bson.M{"age":bson.M{"$in":[]int{1,3,4}}} |
$nin | 不在范围内 | bson.M{"age":bson.M{"$nin":[]int{1,3,4}}} |
https://www.mongodb.com/docs/manual/reference/operator/query/and/
逻辑符号 | 含义 | filter示例 |
---|---|---|
$and | 与 | bson.M{"$and":[]bson.M{{"name":"tom"},{"age":33}}} bson.M{"$and":[]bson.M{{"name":"tom"},{"age":bson.M{"$gt":32}}}} |
$or | 或 | bson.M{"$or": []bson.M{{"name": "tom"}, {"age": bson.M{"$lt":20}}}} |
$not | 非 | bson.M{"age": bson.M{"$not":bson.M{"$gte":20}}} |
bson.M{"age": bson.M{"$gte":20}}
取反为bson.M{"age":bson.M{"$not":bson.M{"$gte":20}}}
元素 | 含义 | 示例 |
---|---|---|
$exists | 文档中是否有这个字段 | bson.M{"Name":bson.M{"$exists":true}} |
$type | 字段是否是指定的类型 | bson.M{"age":bson.M{"$type":16}} |
bson.M{"name":bson.M{"$exists":true}}
标识所有具有Name字段的文档,注意Name和name不一样
常用类型,参考:https://www.mongodb.com/docs/manual/reference/operator/query/type/#op._S_type
- 字符串类型编码为2,别名为string
- 整型编码为16,别名为int
- 长整型编码为18,别名为long
改造函数findByFilter为findAll
func findAll(filter interface{}, opt *options.FindOptions) {
var results []*User
cursor, err := users.Find(context.TODO(), filter, opt)
if err != nil {
log.Fatal(err)
}
defer cursor.Close(context.TODO()) // 关闭游标
err = cursor.All(context.TODO(), &results)
if err != nil {
log.Fatal(err)
}
fmt.Println(results) }
findAll(filter, options.Find().SetLimit(2))
投影
filter := bson.M{"age": bson.M{"$gt": 18}}
opt := options.Find()
opt.SetProjection(bson.M{"name": false, "age": false}) // name、age字段不投影,都显示为零值
findAll(filter, opt)
opt.SetProjection(bson.M{"name": true}) // name投影,age字段零值显示
排序
opt.SetSort(bson.M{"age": 1}) // 升序
opt.SetSort(bson.M{"age": -1}) // 降序
分页
opt.SetSkip(1) // offset
opt.SetLimit(1) // limit
更新
更新操作符 | 含义 | 示例 |
---|---|---|
$inc | 对给定字段数字值增减 | bson.M{"$inc":bson.M{"age": -5}} |
$set | 设置字段值,如果字段不存在则创建 | bson.M{"$set":bson.M{"gender":"M"}} |
$unset | 移除字段 | {'$unset':{'Name':""}} |
// 更新一个
func updateOne() {
filter := bson.M{"age": bson.M{"$exists": true}} // 所有有age字段的文档
update := bson.M{"$inc": bson.M{"age": -5}} // age字段减5
ur, err := users.UpdateOne(context.TODO(), filter, update)
if err != nil {
log.Fatal(err)
}
fmt.Println(ur.MatchedCount, ur.ModifiedCount) }
// 更新多个
func updateMany() {
filter := bson.M{"age": bson.M{"$exists": true}} // 所有有age字段的文档
update := bson.M{"$set": bson.M{"gender": "M"}} // 为符合条件的文档设置gender字段
users.UpdateMany(context.TODO(), filter, update) }
update := bson.M{"$unset": bson.M{"gender": ""}} // 为符合条件的文档移除gender字 段
// 找到一批更新第一个,ReplaceOne更新除ID以外所有字段
filter := bson.M{"age": bson.M{"$exists": true}} // 所有有age字段的文档
replacement := User{Name: "Sam", Age: 48}
ur, err := users.ReplaceOne(context.TODO(), filter, replacement)
if err != nil {
log.Fatal(err) }
fmt.Println(ur.MatchedCount, ur.ModifiedCount)
删除
// 删除一个
func deleteOne() {
filter := bson.M{} // 没有条件,匹配所有文档
dr, err := users.DeleteOne(context.TODO(), filter)
if err != nil {
log.Fatal(err)
}
fmt.Println(dr.DeletedCount) }
// 删除多个
func deleteMany() {
filter := bson.M{} // 没有条件,匹配所有文档
dr, err := users.DeleteMany(context.TODO(), filter)
if err != nil {
log.Fatal(err)
}
fmt.Println(dr.DeletedCount) }
users.DeleteMany(context.TODO(), bson.M{})
删除所有文档,危险!