## MongoDB 和 Mongoose
### mongoose
建立一个 MongoDB Atlas 数据库并导入连接到它所需的软件包。将 `mongodb@~3.6.0` 和 `mongoose@~5.4.0` 添加到项目的 `package.json` 中。 然后,在 `myApp.js` 文件中请求 `mongoose`。 创建一个 `.env` 文件,给它添加一个 `MONGO_URI` 变量。 变量的值为 MongoDB Atlas 数据库 URI。 应用单引号或双引号包裹 URI。请记住,环境变量 `=` 两边不能有空格。 例如应该这样写:`MONGO_URI='VALUE'`。 完成后,使用下面的代码来连接数据库。
```js
mongoose.connect(<Your URI>, { useNewUrlParser: true, useUnifiedTopology: true });
```
```js
const mongoose = require('mongoose');
mongoose.connect("mongodb+srv://<username>:<password>@cluster0.zbwns.mongodb.net/myFirstDatabase?retryWrites=true&w=majority", { useNewUrlParser: true, useUnifiedTopology: true });
```
### 创建模型 Model
**C** RUD 第一小节——CREATE
首先,需要一个 Schema, 每一个 Schema 都对应一个 MongoDB 的 collection, 并且在相应的 collection 里定义 documents 的“样子”。 Schema 用于组成模型(Model), 可以通过嵌套 Schema 来创建复杂的模型。可以根据模型创建实例,模型实例化后的对象称为 documents。
handler 函数会在特定事件(比如调用服务器 API)发生时执行。 `done()` 是一个回调函数,它的作用是在一个异步操作(比如对数据库进行插入、查询、更新或删除)执行完成时,告知可以继续执行后续的其它代码。 这与 Node.js 中的处理方式十分类似,在 Node.js 中,在(异步操作)成功时调用 `done(null, data)`,在失败时调用 `done(err)`。
注意:与远程服务器进行交互时,需要考虑到发生错误的可能!
```js
/* Example */
const someFunc = function(done) {
//... do something (risky) ...
if (error) return done(error);
done(null, result);
};
```
按下面的原型信息创建一个名为 `personSchema` 的 schema:
```markup
- Person Prototype -
--------------------
name : string [required]
age : number
favoriteFoods : array of strings (*)
```
采用 Mongoose 基础 schema 类型。 如果还想添加更多的键,就请使用 required 或 unique 等简单的验证器(validators),并设置默认值。 详情请参考 [Mongoose 文档](http://mongoosejs.com/docs/guide.html)。
请从 `personSchema` 创建一个名为 `Person` 的 model。
```js
const Schema = mongoose.Schema;
const personSchema = new Schema({
name: { type: String, required: true },
age: Number,
favoriteFoods: [String]
});
const Person = mongoose.model("Person", personSchema);
```
### 创建并保存一条 Model 记录
在 `createAndSavePerson` 函数中,使用在上一个挑战中写好的 `Person` 构造函数创建 document 实例, 将包含 `name`、`age` 和 `favoriteFoods` 的对象传给构造函数, 这些属性的数据类型必须符合在 `personSchema` 中定义的类型。 然后在返回的 document 实例上调用方法 `document.save()`。 同时,按 Node.js 的方式为它传一个回调函数。 这是一种常见模式,以下所有 CRUD 方法都将这样的回调函数作为最后一个参数。
```js
/* Example */
// ...
person.save(function(err, data) {
// ...do your stuff here...
});
```
```js
let createAndSavePerson = function(done) {
let janeFonda = new Person({name: "Jane Fonda", age: 84, favoriteFoods: ["eggs", "fish", "fresh fruit"]});
janeFonda.save(function(err, data) {
if (err) return console.error(err);
done(null, data)
});
};
```
### model.create() 创建多条记录
在一些情况下,比如进行数据库初始化,会需要创建很多 model 实例来用作初始数据。 `Model.create()` 接受一组像 `[{name: 'John', ...}, {...}, ...]` 的数组作为第一个参数,并将其保存到数据库。
```js
let arrayOfPeople = [
{name: "Frankie", age: 74, favoriteFoods: ["Del Taco"]},
{name: "Sol", age: 76, favoriteFoods: ["roast chicken"]},
{name: "Robert", age: 78, favoriteFoods: ["wine"]}
];
let createManyPeople = function(arrayOfPeople, done) {
Person.create(arrayOfPeople, function (err, people) {
if (err) return console.log(err);
done(null, people);
});
};
```
### model.find() 查询数据库
C **R** UD 第二小节—— Read 查询
尝试一种最简单的用法,`Model.find()` 接收一个查询 document(一个 JSON 对象)作为第一个参数,一个回调函数作为第二个参数, 它会返回由匹配到的数据组成的数组。 这个方法支持很多搜索选项, 详情请参阅文档。
```js
let findPeopleByName = function(personName, done) {
Person.find({name: personName}, function (err, personFound) {
if (err) return console.log(err);
done(null, personFound);
});
};
```
### model.findOne() 返回一个单一匹配
`Model.findOne()` 与 `Model.find()` 十分类似,但就算数据库中有很多条数据可以匹配查询条件,它也只返回一个 document,而不会返回一个数组, 如果查询条件是声明为唯一值的属性,它会更加适用。
```js
let findOneByFood = function(food, done) {
Person.findOne({favoriteFoods: food}, function (err, data) {
if (err) return console.log(err);
done(null, data);
});
};
```
### model.findById() 根据 _id 搜索
在保存 document 的时候,MongoDB 会自动为它添加 `_id` 字段,并给该字段设置一个唯一的仅包含数字和字母的值。 通过 `_id` 搜索是一个十分常见的操作,为此,Mongoose 提供了一个专门的方法。
```js
var findPersonById = function(personId, done) {
Person.findById(personId, function (err, data) {
if (err) return console.log(err);
done(null, data);
});
};
```
### 通过执行查询、编辑、保存来执行经典更新流程
CR **U** D 第三小节—— Update 更新
在过去,如果想要编辑 document 并以某种方式使用它(比如放到服务器的返回数据中),就必须执行查找、编辑和保存。 Mongoose 有一个专用的更新方法 `Model.update()`, 它与底层的 mongo 驱动绑定。 通过这个方法,可以批量编辑符合特定条件的多个 document。但问题在于,这个方法不会返回更新后的 document,而是返回状态信息。 此外,它直接调用 mongo 的底层驱动,让处理 model 的验证变得更加棘手。
```js
const findEditThenSave = (personId, done) => {
const foodToAdd = 'hamburger';
Person.findById(personId, (err, person) => {
if(err) return console.log(err); person.favoriteFoods.push(foodToAdd);
person.save((err, updatedPerson) => {
if(err) return console.log(err);
done(null, updatedPerson)
})
})
};
```
### 在 document 中执行新的更新方式——使用 model.findOneAndUpdate()
最近发布的 mongoose 版本简化了 document 的更新方式, 但同时,一些高级功能(如 pre/post hook, 验证)的使用方式也变得和以前不同。因此,在很多情景下,上一个挑战中提到的老方法其实更常用。 新方法的加入,可以让我们使用 `findByIdAndUpdate()` 来进行基于 id 的搜索。
```js
const findAndUpdate = (personName, done) => {
const ageToSet = 20;
Person.findOneAndUpdate({name: personName}, {age: ageToSet}, {new: true}, (err, updatedDoc) => {
if(err) return console.log(err);
done(null, updatedDoc);
})
};
```
**提示:** 需要返回更新后的 document。 可以把 `findOneAndUpdate()` 的第三个参数设置为 `{ new: true }` 。 默认情况下,这个方法会返回修改前的数据。
### model.findByIdAndRemove 删除一个 document
CRU **D** 第四小节—— Delete 删除
`findByIdAndRemove` 和 `findOneAndRemove` 类似于之前的更新方法, 它们将被删除的 document 传给数据库。 和之前一样,使用函数参数 `personId` 作为查询关键字。
修改 `removeById` 函数,通过 `_id` 删除一个人的数据, 可以使用 `findByIdAndRemove()` 或 `findOneAndRemove()` 方法。
```js
let removeById = function(personId, done) {
Person.findByIdAndRemove(
personId,
(err, removedDoc) => {
if(err) return console.log(err);
done(null, removedDoc);
}
);
};
```
### model.remove() 删除多个 document
`Model.remove()` 可以用于删除符合给定匹配条件的所有 document。
修改 `removeManyPeople` 函数,使用 `nameToRemove` 删除所有姓名是变量 `Model.remove()` 的人。 给它传入一个带有 `name` 字段的查询 document 和一个回调函数。
**注意:** `Model.remove()` 不会返回被删除的 document,而是会返回一个包含操作结果以及受影响的数据数量的 JSON 对象。 不要忘记将它传入 `done()` 回调函数,因为我们需要在挑战的测试中调用它。
```js
const removeManyPeople = (done) => {
const nameToRemove = "Mary";
Person.remove({name: nameToRemove}, (err, response) => {
if(err) return console.log(err);
done(null, response);
})
};
```
### 链式调用辅助查询函数来缩小搜索结果
如果不给 `Model.find()`(或者别的搜索方法)的最后一个参数传入回调函数, 查询将不会执行。 可以将查询条件存储在变量中供以后使用, 也可以通过链式调用这类变量来构建新的查询字段。 实际的数据库操作会在最后调用 `.exec()` 方法时执行。 必须把回调函数传给最后一个方法。 Mongoose 提供了许多辅助查询函数, 这里使用最常见的一种。
修改 `queryChain` 函数来查询喜欢 `foodToSearch` 食物的人。 同时,需要将查询结果按 `name` 属性排序, 查询结果应限制在两个 document 内,并隐藏 age 属性。 请链式调用 `.find()`、`.sort()`、`.limit()` 和 `.select()`,并在最后调用 `.exec()`, 并将 `done(err, data)` 回调函数传入 `exec()`。
```js
let queryChain = function(done) {
var foodToSearch = "burrito";
var jsonObject = {favoriteFoods : foodToSearch};
Person.find(jsonObject).sort({name: 1}).limit(2).select({age: 0}).exec((err, data) => {
(err) ? done(err) : done(null, data);
})
};
```