MongoDB基础和面试必备

603 阅读10分钟

1.问题梳理

什么是Mongoose?它与MongoDB之间的关系是什么?

Mongoose是一个基于Node.js的ODM(对象文档映射器),利用它可以更方便地操作MongoDB数据库。它可以让开发者更容易定义Schema、执行查询、进行关联数据读写等操作。但是它并不是直接连接MongoDB数据库,而是使用它自己的连接器(MongoDB驱动程序)与MongoDB数据库进行通信。

如何定义一个简单Mongoose Schema?

一个简单的Mongoose Schema定义如下:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const userSchema = new Schema({
  name: { type: String, required: true },
  
  email: { type: String, required: true, unique: true },
  age: Number,
  created_at: { type: Date, default: Date.now() }
});

Mongoose Schema的定义包含三个主要部分:字段名、字段类型和字段选项。对于每个字段,都可以指定一个或多个选项,如requireduniquedefault等。Schema的创建是通过调用mongoose.Schema()构造函数完成的。

如何使用Mongoose创建一个新的文档?

Mongoose提供了两种不同的方法来将数据保存到数据库中:save() 和 create()save()方法是通过先创建一个新的文档对象,再调用实例对象的save()方法保存到MongoDB数据库中。而create()方法直接创建文档对象并将其保存到数据库中。

以下是一个使用save()创建新文档的示例代码:

const UserModel = mongoose.model('User', userSchema);
const newUser = new UserModel({
  name: 'Alice',
  email: 'alice@example.com',
  age: 28
});
newUser.save(function(err) {
  if (err) {
    console.log(err);
  } else {
    console.log('New user created');
  }
});

以下是一个使用create()创建新文档的示例代码:

UserModel.create({ 
  name: 'Alice',
  email: 'alice@example.com',
  age: 28 
}, function(err, user) {
  if (err) {
    console.log(err);
  } else {
    console.log('New user created');
  }
});

Mongoose中的中间件(Middleware)有什么作用?

Mongoose中的中间件可以在执行前后、保存前后、删除前后等不同的生命周期环节中插入代码。它的主要作用在于实现状态验证、数据转换、数据预处理等功能,简化开发者的操作。以下是一个例子:

userSchema.pre('save', function(next) {
  const currentDate = new Date();
  this.updated_at = currentDate;
  if (!this.created_at) {
    this.created_at = currentDate;
  }
  next();
});

在文档保存前自动添加created_atupdated_at属性。

Mongoose中的中间件按照作用对象的不同可以分为四种:

  • Document Middleware、
  • Model Middleware、
  • Aggregation Middleware
  • Query Middleware。

其中Document Middleware又分为以下三种类型:pre('validate')pre('save')pre('remove'),分别在文档验证、文档保存和文档删除操作之前执行指定的中间件函数。以下是一个使用Document Middleware增加username属性并将email属性转换为小写的示例代码:

userSchema.pre('validate', function(next) {
  if (this.email) {
    this.username = this.email.split('@')[0];
    this.email = this.email.toLowerCase();
  }
  next();
});

如何使用Mongoose进行数据查询?

使用Mongoose进行数据查询的方法包括:Model.findOne()、Model.find()、Model.findById()、Model.countDocuments()等方法。其中findOne()方法返回单个文档,find()方法返回一组文档,findById()方法按照id查询单个文档,countDocuments()返回文档数目。以下是一个查询年龄大于20的所有用户的示例代码:

UserModel.find({ age: { $gt: 20 } }, function(err, users) {
  if (err) {
    console.log(err);
  } else {
    console.log(users);
  }
});

如何使用Mongoose进行文档更新?

文档更新可通过调用Model.updateOne()Model.updateMany()Model.findOneAndUpdate()Model.findByIdAndUpdate()等方法实现。其中updateOne()updateMany()方法返回更新文档数,findOneAndUpdate()方法返回更新前文档、findByIdAndUpdate()方法按照id更新单个文档。以下是一个将年龄大于20的用户的年龄更新为30的示例代码:

UserModel.updateMany({ age: { $gt: 20 } }, { $set: { age: 30 } }, function(err, users) {
  if (err) {
    console.log(err);
  } else {
    console.log(users);
  }
});

如何使用Mongoose进行数据关联?

Mongoose使用populate()方法实现关联数据读取,它可以将一个或多个文档的引用字段填充成对应文档的对象或对象数组。以下是一个基于两个Schema(User、Order)的关联查询示例代码:

const OrderSchema = new Schema({
  user: { type: Schema.Types.ObjectId, ref: 'User' },
  product: { type: String, required: true }
});

const UserModel = mongoose.model('User', userSchema);
const OrderModel = mongoose.model('Order', orderSchema);

OrderModel.find().populate('user').exec(function(err, orders) {
  if (err) {
    console.log(err);
  } else {
    console.log(orders);
  }
});

2.使用流程

 //1. 安装 mongoose
 //2. 导入 mongoose
 const mongoose = require('mongoose');
​
 //3. 连接数据库                              数据库名称
 mongoose.connect('mongodb://127.0.0.1:27017/database');
​
 //4. 设置连接回调
 //连接成功   once 一次   事件回调函数只执行一次
mongoose.connection.on('open', () => {
    console.log('连接成功');
    //5. 创建文档结构对象
    // 设置集合中 文档的属性以及属性值得类型
    let BookSchema = new mongoose.Schema({
        title: String,
        author: String,
        price: Number
    });
     
    //6. 创建文档模型对象  对文档操作的封装对象  mongoose会使用集合名称的复数,创建集合
    let BookModel = mongoose.model('book', BookSchema);
    
     //7. 插入文档
    BookModel.create({
        title: '完美世界',
        author: '辰东',
        price: 19.9
    }, (err, data) => {
        // 判断是否有错误
        if (err) throw err;
        //输出 data 对象  如果没有出错,则输出插入后的文档对象
        console.log(data);
        //8. 断开连接  关闭数据链接 (项目运行过程中,不会添加该代码)
        mongoose.disconnect();
    });
});
​
//连接出错
mongoose.connection.on('error', () => {
    console.log('连接出错~~');
})
​
//连接关闭
mongoose.connection.on('close', () => {
    console.log('连接关闭');
})

以下是一个简单的示例,展示如何使用Node.js和Mongoose启动并连接到MongoDB数据库,然后定义和创建一个名为"book"的数据模型,并使用该模型来进行CRUD操作:

// 引入mongoose模块
const mongoose = require('mongoose'); 

// 建立MongoDB连接
mongoose.connect('mongodb://localhost/myapp', {
	useNewUrlParser: true,
	useUnifiedTopology: true
}).then(() => {
	console.log('MongoDB连接成功');
}).catch(error => {
	console.log(`MongoDB连接失败: ${error}`);
});

// 定义数据模型架构
const BookSchema = new mongoose.Schema({
	title: String,
	author: String,
	price: Number
});

// 创建数据模型
const BookModel = mongoose.model('book', BookSchema);

// 创建一本书
const myBook = new BookModel({
	title: 'Node.js入门',
	author: '小明',
	price: 50
});
myBook.save((error) => {
	if (error) return console.error(error);
	console.log('书籍已保存到数据库');
});

// 查询所有书籍
BookModel.find({}, (error, books) => {
	if (error) return console.error(error);
	console.log('所有书籍:', books);
});

// 更新一本书籍
BookModel.findOneAndUpdate({title: 'Node.js入门'}, {price: 60}, (error, book) => {
	if (error) return console.error(error);
	console.log('更新后的书籍:', book);
});

// 删除一本书籍
BookModel.deleteOne({title: 'Node.js入门'}, (error) => {
	if (error) return console.error(error);
	console.log('书籍已从数据库中删除');
});

在这个例子中,我们通过mongoose.connect()函数建立与MongoDB数据库的连接,并使用BookSchema对象定义了一个名为"book"的数据模型。我们可以使用BookModel对象创建、查询、更新和删除"book"集合中的记录。这些操作都是通过mongoose底层函数执行的。当然,还有其他更复杂的操作可以使用MongoDB提供的更高级的功能,包括聚合查询、事务、地理位置查询等。

3.字段类型及验证

文档结构可选的常用字段类型列表

类型描述
String字符串
Number数字
Boolean布尔值
Array数组,也可以使用 [] 来标识
Date日期
BufferBuffer 对象
Mixed任意类型,需要使用 mongoose.Schema.Types.Mixed 指定
ObjectId对象 ID,需要使用 mongoose.Schema.Types.ObjectId 指定
Decimal128高精度数字,需要使用 mongoose.Schema.Types.Decimal128 指定

字段值验证

Mongoose 有一些内建验证器,可以对字段值进行验证

1.必填项

title: {
    type: String,
    required: true // 设置必填项
},

2.默认值

author: {
    type: String,
    default: '匿名' //默认值
},

3.枚举值

gender: {
    type: String,
    enum: ['男','女'] //设置的值必须是数组中的
},

4.唯一值

username: {
    type: String,
    unique: true
},

unique 需要 重建集合 才能有效果

永远不要相信用户的输入

4.CRUD

数据库的基本操作包括四个,增加(create),删除(delete),修改(update),查(read)

增加

插入一条

SongModel.create({
    title:'给我一首歌的时间',
    author: 'Jay'
}, function(err, data){
    //错误
    console.log(err);
    //插入后的数据对象
    console.log(data);
});

批量插入

//1.引入mongoose
const mongoose = require('mongoose');
​
//2.链接mongodb数据库 connect 连接
mongoose.connect('mongodb://127.0.0.1:27017/project');
​
//3.设置连接的回调
mongoose.connection.on('open',()=>{
    //4.声明文档结构
    const PhoneSchema = new mongoose.Schema({
        brand:String,
        color:String,
        price:Number,
        tags:Array
    })
    //6.创建模型对象
    const PhoneModel = mongoose.model('phone',PhoneSchema);
    PhoneModel.insertMany([
        {
            brand:'华为',
            color:'灰色',
            price:2399,
            tags:['电量大','屏幕大','信号好']
        },
        {
            brand:'小米',
            color:'白色',
            price:2099,
            tags:['电量大','屏幕大','信号好']
         }
    ],(err,data)=>{
        if(err) throw err;
         console.log('写入成功');
         mongoose.connection.close();
    })
})

删除

删除一条数据

SongModel.deleteOne({_id:'5dd65f32be6401035cb5b1ed'}, function(err){
    if(err) throw err;
    console.log('删除成功');
    mongoose.connection.close();
});

批量删除

SongModel.deleteMany({author:'Jay'}, function(err){
    if(err) throw err;
    console.log('删除成功');
    mongoose.connection.close();
});

使用deleteOne()或deleteMany()命令删除数据。以下是一个简单的示例代码:

// 连接MongoDB数据库
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017/mydb';
MongoClient.connect(url, function(err, db) {
  if (err) throw err;

  // 删除数据
  const dbo = db.db("mydb");
  const myquery = { address: 'Mountain 21' };
  dbo.collection("customers").deleteOne(myquery, function(err, obj) {
    if (err) throw err;
    console.log("1 document deleted");
    db.close();
  });
});

更新

更新一条数据

SongModel.updateOne({author: 'JJ Lin'}, {author: '林俊杰'}, function (err) {
    if(err) throw err;
    mongoose.connection.close();
});

批量更新数据

SongModel.updateMany({author: 'Leehom Wang'}, {author: '王力宏'}, function (err) {
    if(err) throw err;
    mongoose.connection.close();
});

可以使用updateOne()或updateMany()命令更新数据。以下是一个简单的示例代码:

// 连接MongoDB数据库
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017/mydb';
MongoClient.connect(url, function(err, db) {
  if (err) throw err;

  // 更新数据
  const dbo = db.db("mydb");
  const myquery = { address: "Highway 37" };
  const newvalues = { $set: {name: "Mickey", address: "Canyon 123" } };
  dbo.collection("customers").updateOne(myquery, newvalues, function(err, res) {
    if (err) throw err;
    console.log("1 document updated");
    db.close();
  });
});

查询

查询一条数据

SongModel.findOne({author: '王力宏'}, function(err, data){
    if(err) throw err;
    console.log(data);
    mongoose.connection.close();
});
//根据 id 查询数据
SongModel.findById('5dd662b5381fc316b44ce167',function(err, data){
    if(err) throw err;
    console.log(data);
    mongoose.connection.close();
});

查询一个文档中的多个字段?

db.collection.find(查找条件,{key1:1, key2:1, ...});

示例

db.students.find({name:"张三"},{age:1, gender:1});

批量查询数据

//不加条件查询
SongModel.find(function(err, data){
    if(err) throw err;
    console.log(data);
    mongoose.connection.close();
});
//加条件查询
SongModel.find({author: '王力宏'}, function(err, data){
    if(err) throw err;
    console.log(data);
    mongoose.connection.close();
});

MongoDB使用find()命令查询数据,可以使用条件运算符和逻辑运算符筛选数据。以下是一个简单的示例代码:

// 连接MongoDB数据库
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017/mydb';
MongoClient.connect(url, function(err, db) {
  if (err) throw err;

  // 查询数据
  const dbo = db.db("mydb");
  dbo.collection("customers").find({}).toArray(function(err, result) {
    if (err) throw err;
    console.log(result);
    db.close();
  });
});

5.条件控制

运算符

在 mongodb 不能 > < >= <= !== 等运算符,需要使用替代符号

  • >使用 $gt
  • < 使用 $lt
  • = 使用 $gte
  • <= 使用 $lte
  • !== 使用 $ne
db.students.find({id:{$gt:3}}); id号比3大的所有的记录
​
// 价格小于 20 的图书
BookModel.find({ price: { $lt: 20 } }, (err, data) => {
    if (err) {
        console.log('读取失败~~~')
        return
    }
    console.log(data)
})

逻辑运算 and 和 or 查询条件

Mongoose中可以使用andand和or运算符来组合多个条件查询,如下所示:

const Book = mongoose.model('book', BookSchema);

// 查询作者为'小明'且价格小于50元的书籍
Book.find({ $and: [ { author: '小明' }, { price: { $lt: 50 } } ] }, (err, books) => {
  if (err) return console.error(err.message);
  console.log(`查询结果:${books}`);
});

// 查询作者为'小明'或价格小于50元的书籍
Book.find({ $or: [ { author: '小明' }, { price: { $lt: 50 } } ] }, (err, books) => {
  if (err) return console.error(err.message);
  console.log(`查询结果:${books}`);
});

以上代码展示了如何使用Mongoose的andand和or运算符来进行多条件组合查询。在这个例子中,我们使用了find()方法来执行查询操作。

6.个性化读取

正则匹配

条件中可以直接使用 JS 的正则语法,通过正则可以进行模糊查询

db.students.find({name:/imissyou/});
​
正则表达式,搜索书籍名称中带有 '三' 的图书
BookModel.find({ name: /三/ }, (err, data) => {
    if (err) {
        console.log('读取失败~~~')
        return
    }
   console.log(data)
})
​
BookModel.find({ name: new RegExp('三') }, (err, data) => {
    if (err) {
        console.log('读取失败~~~')
        return
    }
    console.log(data)
})

字段筛选

//0:不要的字段
//1:要的字段
SongModel.find().select({_id:0,title:1}).exec(function(err,data){
    if(err) throw err;
    console.log(data);
    mongoose.connection.close();
});

数据截取

//skip 跳过   limit 限定
SongModel.find().skip(10).limit(10).exec(function(err,data){
    if(err) throw err;
    console.log(data);
    mongoose.connection.close();
});

分页查询

Mongoose中可以使用skip()方法跳过记录,使用limit()方法获取指定数量的记录,来进行分页查询,如下所示:

const Book = mongoose.model('book', BookSchema);

// 分页查询书籍记录
const pageNum = 1;   // 当前页码
const pageSize = 10; // 每页记录数
const skipNum = (pageNum - 1) * pageSize;

Book.find().skip(skipNum).limit(pageSize).exec((err, books) => {
  if (err) return console.error(err.message);
  console.log(`查询结果(第${pageNum}页,每页${pageSize}条记录):${books}`);
});

以上代码展示了如何使用Mongoose的skip()和limit()方法进行分页查询。在这个例子中,我们假设一页显示10条记录,通过查询条件和skip()方法来计算当前页应该跳过的记录数量,然后使用limit()方法获取当前页的记录数量。

排序查询

Mongoose中可以使用sort()方法对查询结果进行排序,如下所示:

const Book = mongoose.model('book', BookSchema);

// 按价格从低到高排序查询所有书籍
Book.find().sort({ price: 1 }).exec((err, books) => {
  if (err) return console.error(err.message);
  console.log(`查询结果:${books}`);
});

// 按价格从高到低排序查询所有书籍
Book.find().sort({ price: -1 }).exec((err, books) => {
  if (err) return console.error(err.message);
  console.log(`查询结果:${books}`);
});

以上代码展示了如何使用Mongoose的sort()方法进行排序查询。在这个例子中,我们使用了find()方法来执行查询操作。在sort()方法中,我们可以使用1表示升序排序,使用-1表示降序排序。

updateMany()方法

Mongoose可以使用updateMany()方法来更新多个文档,如下所示:

const Book = mongoose.model('book', BookSchema);

// 更新所有书的价格
Book.updateMany({}, { $set: { price: 30 } }, (err, result) => {
  if (err) return console.error(err.message);
  console.log(`${result.nModified}条记录已更新`);
});

7.高级查询和操作示例:

聚合查询

// 求学生平均年龄
Student.aggregate(
  [
    {
        $match: {
          created_at: { $gte: new Date('2023-01-01'), $lte: new Date('2023-01-31') }
        }
      },
      {
        $group: {
          _id: { $dateToString: { format: '%Y-%m-%d', date: "$created_at" } },
          active_users: { $sum: 1 }
        }
      }
  ],
  (err, res) => {
    if (err) return console.error(err.message);
    console.log(`学生平均年龄为:${res[0].avgAge}`);
  }
);

以上示例中,使用MongoDB的groupgroup和avg运算符来进行聚合查询,计算学生的平均年龄。在Mongoose中,我们可以使用aggregate()方法来执行聚合查询。

文本搜索

// 搜索含有"name"字段的所有文档
Student.find({ $text: { $search: 'name' } }, (err, students) => {
  if (err) return console.error(err.message);
  console.log(`搜索结果:${students}`);
});

以上示例中,使用MongoDB的text运算符和text运算符和search表达式来进行文本搜索,查询所有含有"name"字段的文档。在Mongoose中,我们可以使用find()方法并在查询条件上添加"text""text"和"search"查询参数来执行文本搜索。

事务

// 开始事务
const session = await mongoose.startSession();
session.startTransaction();

try {
  // 先查询原始数据
  const student = await Student.findOne({ name: 'Tom' }).session(session);

  // 进行一些操作
  await doSomethingWithStudent(student);

  // 将更改写入数据库
  await session.commitTransaction();
  console.log('事务执行成功');
} catch(err) {
  // 回滚事务
  await session.abortTransaction();
  console.error('事务执行失败:' + err.message);
} finally {
  session.endSession();
}

以上示例中,使用async/await方法和Mongoose的startSession()函数、session.startTransaction()、session.commitTransaction()和session.abortTransaction()来执行事务操作。在事务中,我们可以执行多个操作,而要么全部成功,要么全部回滚。在代码块开始时,我们先创建一个会话对象,然后使用该对象开始事务。如果所有操作成功,我们使用commitTransaction()方法提交所有更改。如果出现错误,则使用abortTransaction()方法回滚所有更改。在finally子句中,我们使用endSession()方法结束会话。

索引

MongoDB中可以使用索引来提高查询性能。Mongoose中可以使用index()方法定义索引,如下所示:

const BookSchema = new mongoose.Schema({
  title: String,
  author: String,
  price: Number,
  published: Date
});

// 给title字段添加唯一索引
BookSchema.index({ title: 1 }, { unique: true });

// 给published字段添加普通索引
BookSchema.index({ published: 1 });

const Book = mongoose.model('book', BookSchema);

**

以上代码展示了如何在Mongoose中使用index()方法定义索引。在这个例子中,我们使用了title字段和published字段定义了两个索引,其中title字段的索引是唯一的,published字段的索引是普通的。

迁移数据

在MongoDB中迁移数据时,常常需要将数据从一个集合或数据库转移到另一个集合或数据库中。Mongoose可以使用一些API来实现数据迁移,如下所示:

// 将student集合的数据迁移到newStudents集合
const Student = mongoose.model('student', StudentSchema);
const newStudentSchema = new mongoose.Schema({ name: String, age: Number }); // 目标集合的schema
const NewStudent = mongoose.model('newStudents', newStudentSchema);  // 目标集合的model

Student.find({}, (err, students) => {
  if (err) return console.error(err.message);

  const newStudents = students.map((student) => {
    return { name: student.name, age: student.age };
  });

  NewStudent.collection.insertMany(newStudents, (err, result) => {
    if (err) return console.error(err.message);
    console.log(`${result.insertedCount} records inserted`);
  });
});

**

以上代码展示了如何使用Mongoose将数据从一个集合转移到另一个集合。在这个例子中,我们使用Student集合的数据,创建了一个新的NewStudent集合,然后将数据从Student迁移到NewStudent。在迁移过程中,我们需要为NewStudent定义一个相应的Schema,并使用map()方法将数据映射为符合目标集合Schema的格式(也就是只包含name和age字段)。最后,我们使用insertMany()方法将数据插入到NewStudent集合中。

8.构建MongoDB分布式集群

MongoDB可以通过搭建分布式集群来实现负载均衡和高可用性,将其部署在多个服务器上,并且它们协调工作以提供高可用性和水平扩展性。以下是一些构建MongoDB分布式集群的步骤:

以下是一个构建MongoDB分布式集群的Node.js代码示例:

1. 安装MongoDB Node.js驱动程序

npm install mongodb --save

2. 启动MongoDB服务

为了测试集群,你需要在本地或远程服务器上启动MongoDB服务

3. 连接MongoDB服务器

const MongoClient = require('mongodb').MongoClient;
const uri = 'mongodb://mongo-node1.example.com:27017,mongo-node2.example.com:27017,mongo-node3.example.com:27017/mydb?replicaSet=my-repl-set';
const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });
client.connect(err => {
  if (err) {
    console.log('Error connecting to MongoDB cluster: ', err);
  } else {
    console.log('Successfully connected to MongoDB cluster!')
  }
  client.close();
});

在这个示例中,我们在mongodb:// URI中指定了复制集名称my-repl-set,同时还列出了所有MongoDB节点的名称和端口。请根据你的实际环境更改URI。

4. 测试MongoDB集群

为了测试MongoDB集群,请执行一些查询和插入操作:

const collection = client.db("mydb").collection("mycollection");
collection.insertOne({name: 'Test', value: 42}, function(err, result) {
  if (err) {
    console.log('Error inserting document: ', err);
  } else {
    console.log('Successfully inserted document!');
    collection.findOne({name: 'Test'}, function(err, document) {
      if (err) {
        console.log('Error finding document: ', err);
      } else {
        console.log('Found document: ', document);
      }
    });
  }
});

在本例中,我们向集合中插入一个新文档,然后从集合中查询这个文档。如果出现任何问题,如连接问题或操作失败,将在控制台中打印错误消息。注意,由于我们在连接字符串中启用了useUnifiedTopology 选项,在提供符合复制集名称的连接字符串后,驱动程序将使用内置副本设置发现功能自动检测集群的拓扑结构。