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 如果字段类型为int64uint32uint64 类型,并且该字段的值可以放入有符号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{}) 删除所有文档,危险!