mongodb学习

注意,这里所讨论的都是MongoDB4版本

一、基本概念

1、对于mongodb4来说,它默认就是直接开机启动的了,也就是说并不需要像之前的那样先运行mongod命令,接着才能使用mongo命令访问数据库。

2、一些基础命令:

  • 1.创建新的数据库:use dbName,存在该数据库的话那么便连接到该数据库,不存在数据库的话那么便创建数据库。需要注意的时,利用他创建数据库,数据库并不会立马展示出来,需要先往里面插入一些数据才行。

  • 2.查看所有数据库:show dbs。

  • 3.往数据库里面插入内容:db.databaseName.insert(Obj),便可以插入一些数据。

  • 4.查看某个数据库有权被哪些用户访问:show users.

  • 5.让某个用户失去访问该数据库的权限:db.dropUser(‘admin’).

二、MongoDB权限配置

默认的话,mongoDb安装后,是直接使用一个mongod命令就能连接数据库的,连密码都不用输入,很显然,这种做法很不安全,所以配置权限对于MongoDB是很有必要的。配置权限需要经过下面几个步骤(这里以配置超级管理员用户为例):

1、创建一个超级管理员用户

1
2
3
4
5
6
7
use admin        # 安装了MongoDB的话,那么这个数据库一定会存在的

db.createUser({
user: 'hahahai',
pwd: 'hahaha this is a test pwd',
roles: [{ role: 'root', db: 'admin' }]
})

经过上面这一步后,还是可以直接使用mongo命令连接数据库的,要让权限设置应用起来的话,那么还需要经过下面这一步骤:找到MongoDB的权限配置文件,开启安全验证:

1
2
security:
authorization: enabled

在开启验证之后,接着需要重启一下MongoDB。开启权限验证并重启后,如果你继续使用mongo命令的话,那么此时的确是可以连接上的,但是你想做其它任何操作都将报一个没有权限的错误。那么如何以指定用户登录呢,举例如下所示:

1
mongo admin -u hahahai -p hahahai this is a test pwd

对于Mac来说,使用mongod –auth命令告诉MongoDB开启权限验证。

三、关系型数据库中表与表之间的三种关系

1、一对一关系:假设存在一个驾驶证表和一个身份证表,那么驾驶证表和身份证表就是属于一对一的关系。很多时候,表之间的一对一关系都可以对其进行合并为一张表,但是分成两张表也叫做垂直拆分,这会有助于性能优化。

2、一对多关系:比如说新闻分类表和新闻表,一个新闻分类可以拥有多个新闻,这就属于一对多的关系。

3、多对多关系:比如说商品表和用户表就属于多对多的关系,一个商品可以被多个用户收藏,一个用户也可以收藏多个商品,他们之间的这种关系就叫做多对多的关系。对于多对多的关系表来说,一般还会设计一个中间表,这个表所维护的就是一次用户和商品之间的关联,如果要查用户A所收藏的商品的话,那么直接查这个中间表用户名为A的收藏记录即可。

四、MongoDB聚合管道

使用聚合管道可以对集合中的文档进行变换和组合。MongoDB中使用db.COLLECTION_NAME.aggregate()方法来使用聚合管道功能,下面可以感受一下聚合管道的用法。加下来先看一个使用方法,不解释:

1
2
3
db.orders.aggregate([
{ $math: { DOCUMENT_KEY_NAME: '1' } },
]);

常用管道操作符有下面这些:

  • 1.$project: 指明所需要的字段
  • 2.$match: 条件匹配,只有满足条件的文档才会进入下一阶段
  • 3.$limit: 限制结果的数量
  • 4.$skip: 跳过文档的数量
  • 5.$sort: 根据指定条件对文档进行排序
  • 6.$group: 进行分组
  • 7.$lookup: 常用在多张表之间的数据关联

下面来介绍一下具体操作,先创建两个表,分别是order表和order_item表,内容如下所示:

1
2
3
4
5
6
7
8
9
# orders document,可使用db.test.orders.insert插入
{ "order_id": 1, "trade_no": 121211, "status": 1 },
{ "order_id": 2, "trade_no": 121212, "status": 0 },

# order_items document,可使用db.test.order_items.insert插入
{ "order_id": 1, "order_item_id": 999, "goods_name": "apple", "price": 1 },
{ "order_id": 1, "order_item_id": 1000, "goods_name": "cola", "price": 1 },
{ "order_id": 2, "order_item_id": 1001, "goods_name": "lemmon", "price": 2 },
{ "order_id": 2, "order_item_id": 1002, "goods_name": "milk", "price": 0.5 },

需求一: 获取所有订单,但是返回的数据只需要订单号即可,下面介绍两种实现方式,第一种实现方式使用find方法;第二种使用方式使用管道

1
2
3
4
5
6
7
// 使用find
db.test.orders.find({}, {"trade_no": 1})

// 使用管道
db.test.orders.aggregate([
{ $project: { trade_no: 1 } }
])

需求二: 获取状态为已完成的订单

1
2
3
4
5
6
7
// 使用find
db.test.orders.find({ status: 1 })

// 使用管道
db.test.orders.aggregate([
{ $match: { status: 1 }}
])

需求三: 获取已完成状态的订单的订单号列表数据

1
2
3
4
5
// 注意顺序哈,管道是接受前一个的输入,所以一定要注意各个管道之间的运行顺序
db.test.orders.aggregate([
{ $match: { status: 1 } },
{ $project: { trade_no: 1 } },
])

需求四: 获取单价大于等于2的订单item

1
2
3
db.test.order_items.aggregate([
{ $match: { price: { $gte: 2 } } }
])

需求五: 获取订单1的总价,总结作为priceAll字段

1
2
3
4
db.test.order_items.aggregate([
{ $match: { order_id: 1 } },
{ $group: { _id: "$order_id", total: { $sum: "$price" } } },
])

需求六: 按照价格升序获取订单项列表数据

1
2
3
db.test.order_items.aggregate([
{ $sort: { price: 1 } }
])

需求七: 按照价格降序获取订单项列表数据

1
2
3
db.test.order_items.aggregate([
{ $sort: { price: -1 } }
])

需求八: 获取价格最大的那个order_item

1
2
3
4
db.test.order_items.aggregate([
{ $sort: { price: -1 } },
{ $limit: 1 }
])

需求九: 获取价格第二大的那个order_item

1
2
3
4
5
db.test.order_items.aggregate([
{ $sort: { price: -1 } },
{ $skip: 1 },
{ $limit: 1 }
])

需求十: 获取订单1买了哪些商品,买了哪些商品放到items里面

1
2
3
4
5
6
7
8
9
10
11
12
13
db.test.orders.aggregate([
{
$lookup: {
from: 'test.order_items', // 和order_items表进行关联
localField: 'order_id', // orders文档的参照ID
foreignField: 'order_id', // order_items文档的参照ID
as: 'items', // 查到的东西放在items里面
}
},
{
$match: { order_id: 1 }
}
])

五、mongoose学习

mongoose是基于MongoDB的nodejs API进行封装的一个npm包。使用mongoose来操作数据库的大致流程:1.首先为项目安装mongoose库并进行引入;2.使用connect方法连接数据库(具体参数可参考官方文档);3.定义某个collection集合的schema文件;4.根据所定义的schema生成model。下面更加详细的介绍一下上面所描述的各个步骤:

定义schema:

1
2
3
4
5
6
7
8
9
// users.js
const UserSchema = mongoose.Schema({
name: String,
age: Number,
status: { // 默认参数
type: Number,
default: 1
}
});

设计schema就是设计一个MongoDB集合(也就是关系型数据库中的表)。

生成模型model:

1
2
const User = mongoose.model('User', UserSchema);              // 使用方法1
const User = mongoose.model('User', UserSchema, 'users'); // 使用方法二

对于上面的使用方法1来说,我们根据UserSchema这个schema文件创建了users集合,注意这里是users集合,也就是说,我们在定义model的时候,第一个传递的参数基本代表了集合名(更加确切的说法是它是collection name的单数形式,并且首字母必须大写)。

对于使用方法二来说,我们不想因为集合名而造成困惑,那么第三个参数就是确切的指明了这个创建出来的集合名在数据库中的名字。

5.1mongoose的增删改查

1.新增文档逻辑:首先需要根据model实例化出一个文档,接着save即可,如下所示:

1
2
3
4
5
const admin = new User({ name: 'root', age: 1, status: 1});
admin.save(err => {
if (err) { console.error('save error'); }
console.log('success save');
})

2.更新文档

1
2
3
4
User.updateOne({ name: 'root' }, { age: 2 }, err => {
if (err) { console.error('update error'); }
console.log('success update');
});

3.删除文档

1
2
3
4
User.deleteOne({ _id: '23df323fxg34dvfg34gh' }, err => {
if (err) { console.error('delete error'); }
console.log('success delete');
});

4.查找文档

1
2
3
User.find({}, (err, docs) => {
console.log(docs);
});

5.2mongoose预定义模式修饰符

预定义模式修饰符允许我们将数据存储到数据库之前做一个格式处理:

1
2
3
4
5
6
const UserSchema = mongoose.Schema({
name: {
type: String,
trim: true, // 存储之前去掉左右空格,这里的trim就是一个自定义模式修饰符,其它预定义模式修饰符还有lowercase,uppercase
}
});

5.3mongoose自定义修饰符

自定义修饰符有setter和getter,适用于更为复杂的校验环境,举例如下所示:

1
2
3
4
5
6
7
8
9
const UserSchema mongoose.Schema({
name: {
type: String,
set: function(val) {
if (!val) return '';
else return val.trim();
}
}
});

六.mongoose配置索引

通过设置索引可以优化查询速度,但是设置索引也应该适度,过度设置索引将会影响新增操作的逻辑。下面通过在定义schema的时候设置一下索引:

1
2
3
4
5
6
7
const UserSchema = mongoose.Schema({
name: String,
sn: {
type: String,
index: true, // 设置普通索引
}
});

七.给mongoose扩展方法

在mongoose里面既可以对model增加静态方法,也可以对model增加实例方法,下面进行介绍:

1
2
3
4
5
6
7
8
const UserSchema = mongoose.Schema({
name: String,
sn: {
type: String,
index: true
}
});
const User = mongoose.model('User', UserSchema, 'users');

1.静态方法

1
2
3
4
5
6
7
8
9
10
UserSchema.statics.findBySn = function(sn, cb) {
this.find({ sn }, (err, docs) => {
cb && cb(err, docs);
});
}

User.findBySn('121', (err, docs) => {
if (err) { console.error(err); }
console.log(docs);
});

需要注意的地方:如果要实现model的静态方法的话,那么这个静态方法定义在schema.statics对象上面;在静态方法里面,如果想要获取模型的话,那么通过this关键字便可以。

2.实例方法

1
2
3
4
5
6
7
8
9
10
UserSchema.methods.print = function() {
console.log(this);
}

const userAdmin = new User({
name: 'admin',
sns: '0001',
});

userAdmin.print(); // 将会打印出文档自身

如上所示,如果要给model扩展一个实例方法的话,那么需要写在schema.methods对象上面;在实例方法里面,this关键字代表的是document自身。

八.mongoose数据校验

修饰符以及数据校验都是为了实现数据库数据的一致性。下面是一些常用的mongoose内置数据校验器:

  • 1.required: 表示这个field字段是必须传入的
  • 2.max: 只用于Number类型的field,表示这个field的最大值
  • 3.min: 只用于Number类型的field,表示这个field的最小值
  • 4.enum: 只用于String类型的field,表示这个field值的可选集合
  • 5.match: 表示这个field的值需要必须匹配这个正则表达式

下面是一个使用例子:

1
2
3
4
5
6
7
const UserSchema = mongoose.Schema({
name: {
type: String,
trim: true,
required: true
}
});

除了内置的数据校验规则外,mongoose还提供了高级一点的数据校验规则,此时需要搭配validate函数来进行使用,下面是一个例子:

1
2
3
4
5
6
7
8
9
10
const UserSchema = mongoose.Schema({
name: {
type: String,
trim: true,
required: true,
validate: function(val) {
return !val.includes('wc');
}
}
});

九.使用mongoose进行多表查询

这里介绍使用mongoose进行多表查询仍旧以上面的order表和order_item表为先决条件进行介绍。

1.需求1: 查询订单表每个订单买了哪些商品,所买的商品作为goods字段field。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import OrderModel from './model/order';         // order表关联order_item表,那么只需要引入order表

OrderMode.aggregate([
{
$lookup: {
from: 'order_item',
localField: 'order_id',
foreignField: 'order_id',
as: 'goods'
}
}
], (err, docs) => {
if (err) { console.error(err); }
console.log(JSON.stringify(docs));
});

显而易见的,使用mongoose来进行多表关联查询的写法和使用原生mongodb的node API进行查询还是很像的。

2.需求2:获取order_item里面的牛奶存在于哪些订单中(假设牛奶这个document的_id为12sd34fhjk1)

方法一: 使用find进行顺序查询

1
2
3
4
5
6
7
8
9
10
11
import OrderItemModel from './model/order_item';
import OrderModel from './model/order';

OrderItemModel.find({ _id: '12sd34fhjk1' }, (err, docs) => {
if (err) { console.error(err); return; }
let order_id = docs[0].order_id;
OrderMode.find({ order_id }, (err, docs) => {
if (err) { console.error(err); return; }
console.log(docs);
});
});

方法二: 使用aggregate进行多表查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import OrderItemModel from './model/order_item';

OrderItemMode.aggregate([
$lookup: {
from: 'order',
localField: 'order_id',
foreignField: 'order_id',
as: 'order_info'
},
$match: {
_id: mongoose.Types.ObjectId('12sd34fhjk1')
}
], (err, docs) => {
if (err) { console.error(err); return; }
console.log(JSON.stringify(docs));
});

十.mongoose多集合查询

在开始介绍之前,我们先介绍存在如下所示三个collection,分别是articleCategorys, articles, users,下面分别是三个collection的定义:

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
// articleCategorySchema.ts
import * as mongoose from 'mongoose';

const articleCategorySchema = mongoose.Schema({
name: {
type: String,
required: true,
index: true
}
});

// articlesSchema.ts
const articleSchema = mongoose.Schema({
content: {
type: String,
required: true,
},
cid: mongoose.Schema.Types.ObjectId,
uid: mongoose.Schema.Types.ObjectId
});

// userSchema.ts
const userSchema = mongoose.Schema({
name: {
type: String,
required: true
}
});

需求:我们在获取article文档数据的时候,希望连带所对应的category以及userdocument也能够查询出来,下面是通过管道功能来实现这一功能的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Category = mongoose.model('Category', categorySchema, 'categorys');
const Article = mongoose.model('Article', articleSchema, 'articles');
const User = mongoose.model('User', userSchema, 'users');

Article.aggregate([
{
$lookup: {
from: 'categorys',
localField: 'cid',
foreignField: '_id',
as: 'category'
},
$lookup: {
from: 'users',
localField: 'uid',
foreignField: '_id',
as: 'user'
}
}
], (err, docs) => {
if (err) { console.error(err); return; }
console.log(docs);
});

十一.nestjs配合mongoose

下面介绍一下如何在nestjs中使用mongoose:

1.首先安装依赖包@nestjs/mongoose package以及mongoose package。

2.在应用的根模块里面import由@nestjs/mongoose package导出的MongooseModule模块,并且配置连接数据库的信息(使用MongooseModule的forRoot方法,该方法所接受的第一个参数是数据库的地址:可以使用mongod命令查看地址)。在根模块中引入mongoose module并成功配置后,那么应用便和MongoDB建立了连接。

3.在每个功能模块里面,此时依然需要导入MongooseModule模块,同时也要导入schema文件。imports的时候使用forFeature来和具体的某个collection进行关联,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from '../schema/user.schema';
import { UserController } from './user.controller';
import { UserService } from './user/service';

@Module({
imports: [MongooseModule.forFeature([
{
name: 'User',
schema: UserSchema,
collection: 'users',
}
])],
controllers: [UserController],
providers: [UserService]
});

在module里面导入了了mongooseModule之后,那么这个模块的功能文件就都可以得到机会能够读写collection中的数据。

4.在功能模块中关联collection是为了进行读写collection的,根据前面的描述,很显然这部分工作应该交给服务service来做,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';

@Injectable()
export class UserService {
constructor(@InjectModel('Article') private userModel){}

async findAll() {
const result = await this.userModel.find().exec();
return result;
}
}

需要注意的地方,上面InjectModel方法所接受的参数是Article,他是使用forFeature方法的时候所传入的name参数值。