c

将数据存入MongoDB

在讨论在数据库中保存数据的议题之前,我们将看一下调试 Node 应用的几种不同方法。

Debugging Node applications

【调试Node应用】

调试 Node 应用比调试在浏览器中运行的 JavaScript 稍微困难一些。 将数据打印到控制台是一种可靠的方法,而且总是值得一试。 有些人认为应该用更复杂的方法来代替,但我不同意。 即使是世界上最顶尖的开源开发者也会使用 use 这种方法method

Visual Studio Code

Visual Studio Code代码调试器在某些情况下可能很有用。 你可以像这样在调试模式下启动应用:

fullstack content

注意,应用不应该在另一个控制台中运行,否则该端口将会冲突。

下面你可以看到一个屏幕截图,代码执行在保存新便笺的过程中被暂停:

fullstack content

执行在第63行的断点 处停止。 在控制台中,您可以看到note 变量的值。 在左上角的窗口中,您可以看到与应用状态相关的其他内容。

顶部的箭头可用于控制调试器的流。

出于某种原因,我并不经常使用 Visual Studio Code 调试器。

Chrome dev tools

【Chrome开发工具】

利用 Chrome 开发者控制台,通过如下命令启动应用,也可以进行调试:

node --inspect index.js

你可以通过点击 Chrome 开发者控制台中的绿色图标——node的logo,来进入调试器:

fullstack content

调试视图的工作方式与 React 应用相同。Sources 选项卡可用于设置中断点,中断点将暂停代码的执行。

fullstack content

应用的所有 console.log 消息都将出现在调试器的Console 选项卡中。 您还可以检查变量的值并执行自己的 JavaScript 代码。

fullstack content

Question everything

【质疑一切】

调试全栈应用起初可能看起来很棘手。 不久,我们的应用除了前端和后端之外,还将有一个数据库,并且应用中将有许多潜在的 bug。

当应用“不工作”时,我们必须首先找出问题实际发生在哪里。 这个问题存在于一个你没有预料到的地方是很常见的,它可能需要几分钟,几个小时,甚至几天,你才能找到问题的根源。

关键是要有系统性。 既然问题可以存在于任何地方,你就必须质疑每一件事,并逐一排除所有的可能性。 登录到控制台、Postman、调试器和经验都将有所帮助。

当出现 bug 时,所有可能的策略中最糟糕的就是继续编写代码。 这将保证你的代码很快会有更多的bug,并且调试它们将会更加困难。 在这种情况下,丰田生产系统公司(Toyota Production Systems)的 stop and fix 原则也非常有效。

MongoDB

为了永久地存储我们保存的便笺,我们需要一个数据库。 赫尔辛基大学教授的大多数课程都使用关系数据库。 在本课程中,我们将使用MongoDB ,这是一个所谓的文档数据库

文档数据库在组织数据的方式以及它们所支持的查询语言方面不同于关系数据库。 文档数据库通常被归类为NoSQL的术语集。

你可以阅读更多关于文档数据库和 NoSQL 的资料,这些资料来自数据库导论课程的第7周课程。 不幸的是,这些材料目前只有芬兰语版。

现在阅读 MongoDB 手册中关于集合文档的章节,了解文档数据库如何存储数据的基本概念。

当然,您可以在自己的计算机上安装和运行 MongoDB。 然而,互联网上也充满了你可以使用的 Mongo 数据库服务。 在本课程中,我们首选的 MongoDB 提供者将是MongoDB Atlas

一旦你创建并登录到你的账户,Atlas 会建议你创建一个集群:

fullstack content

让我们选择AWS作为提供商,并用Frankfurt作为地区,创建一个集群。

fullstack content

让我们等待集群准备好可以使用。这大约需要10分钟。

注意在集群准备好之前不要继续。

让我们使用database access 选项卡为数据库创建用户凭据。 请注意,这些不是您登录到 MongoDB Atlas 所使用的相同凭据。这是用来让你的应用连接到数据库所用到的。

fullstack content

让我们授予用户读写数据库的权限。

fullstack content

注意 对于某些人来说,新的用户证书在创建后没有立即生效。 在某些情况下,这些凭证需要几分钟的时间才能生效。

接下来,我们必须定义允许访问数据库的 IP 地址。

fullstack content

为了简单起见,我们将允许所有访问的 IP 地址:

fullstack content

最后,我们准备连接到我们的数据库:

fullstack content

选择Connect your application:

fullstack content

该视图显示MongoDB URI,这是我们将提供给我们将添加到应用的 MongoDB 客户端库的数据库地址。

地址是这样的:

mongodb+srv://fullstack:<PASSWORD>@cluster0-ostce.mongodb.net/test?retryWrites=true

我们现在可以使用数据库了。

我们可以通过官方的 MongoDb Node.js 驱动程序库直接从 JavaScript 代码中使用这个数据库,但是使用起来相当麻烦。 相反,我们将使用提供更高级别 API 的Mongoose库。

Mongoose 可以被描述为object document mapper (ODM) ,并且将 JavaScript 对象保存为 Mongo 文档对于Mongoose库来说很简单。

让我们安装 Mongoose:

npm install mongoose --save

现在还不要在后端添加任何处理 Mongo 的代码。 相反,让我们创建mongo.js 文件,来创建一个实践应用:

const mongoose = require('mongoose')

if ( process.argv.length<3 ) {
   console.log('Please provide the password as an argument: node mongo.js <password>')
  process.exit(1)
}

const password = process.argv[2]

const url =
  `mongodb+srv://fullstack:${password}@cluster0-ostce.mongodb.net/test?retryWrites=true`

mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true })

const noteSchema = new mongoose.Schema({
  content: String,
  date: Date,
  important: Boolean,
})

const Note = mongoose.model('Note', noteSchema)

const note = new Note({
  content: 'HTML is Easy',
  date: new Date(),
  important: true,
})

note.save().then(result => {
  console.log('note saved!')
  mongoose.connection.close()
})

注意:取决于你选择了什么区域来构建你的集群, MongoDB URI 可能和上例中提供的有些不同。你应当验证并使用 MongoDB Atlas 生成的正确的URI

该代码假定它将作为命令行参数从我们在 MongoDB Atlas 中创建的凭据中传递密码。 我们可以像这样访问命令行参数:

const password = process.argv[2]

当使用命令node mongo.js password运行代码时,Mongo 将向数据库添加一个新文档。

注意:请注意这里的password 是为数据库用户创建的password,也就是说,如果你的密码是特殊字符,你需要URL encode that password

我们可以从Collections 中查看 MongoDB Atlas 中数据库的当前状态

在概览标签页。

fullstack content

正如视图所指出的那样,匹配便笺的document 已经添加到test 数据库中的notes 集合中。

fullstack content

我们应该给这个数据库起个更好的名字。 正如文档所说,我们可以从 URI 改变数据库的名称:

fullstack content

让我们通过修改 URI,将数据库的名称更改为note-app:

mongodb+srv://fullstack:<PASSWORD>@cluster0-ostce.mongodb.net/note-app?retryWrites=true

让我们再次运行代码。

fullstack content

数据现在存储在正确的数据库中。 该视图还提供了create database 功能,可用于从网站创建新的数据库。 这样创建数据库是没有必要的,因为当应用试图连接到一个尚不存在的数据库时,MongoDB Atlas 会自动创建一个新的数据库。

Schema

在建立到数据库的连接之后,我们为一个便笺定义模式schema和匹配的模型 :

const noteSchema = new mongoose.Schema({
  content: String,
  date: Date,
  important: Boolean,
})

const Note = mongoose.model('Note', noteSchema)

首先,我们定义了存储在 noteSchema 变量中的便笺的模式。 模式告诉 Mongoose 如何将 note 对象存储在数据库中。

在 Note 模型定义中,第一个 "Note"参数是模型的单数名。 集合的名称将是小写的复数 notes,因为Mongoose 约定是当模式以单数(例如Note)引用集合时自动将其命名为复数(例如notes)。

像 Mongo 这样的文档数据库是schemaaless,这意味着数据库本身并不关心存储在数据库中的数据的结构。 可以在同一集合中存储具有完全不同字段的文档。

Mongoose 背后的思想是,存储在数据库中的数据在application 级别上被赋予一个schema ,该模式定义了存储在任何给定集合中的文档的形状。

Creating and saving objects

【创建和保存对象】

接下来,应用在Note model的帮助下创建一个新的 Note 对象:

const note = new Note({
  content: 'HTML is Easy',
  date: new Date(),
  important: false,
})

模型是所谓的构造函数constructor function,它根据提供的参数创建新的 JavaScript 对象。 由于对象是使用模型的构造函数创建的,因此它们具有模型的所有属性,其中包括将对象保存到数据库的方法。

将对象保存到数据库是通过恰当命名的 save 方法实现的,可以通过 then 方法提供一个事件处理程序:

note.save().then(result => {
  console.log('note saved!')
  mongoose.connection.close()
})

当对象保存到数据库时,将调用提供给该对象的事件处理。 事件处理程序使用命令代码 mongoose.connection.close() 关闭数据库连接。 如果连接没有关闭,程序将永远不能完成它的执行。

保存操作的结果存在事件处理程序的结果参数中。 当我们将一个对象存储到数据库时,结果并不那么有趣。 如果希望在实现应用或调试过程中仔细查看对象,可以将该对象打印到控制台。

我们还可以通过修改代码中的数据和再次执行程序来保存更多的便笺。

遗憾的是 Mongoose 文档并不是非常一致,在其示例中使用了回调函数,而在其其他章节又使用了不同的方式,因此不建议直接从那里复制粘贴代码。 不建议在同一代码中将承诺与老式的回调混合使用。

Fetching objects from the database

【从数据库中获取对象】

让我们注释掉生成新便笺的代码,并用如下代码替换它:

Note.find({}).then(result => {
  result.forEach(note => {
    console.log(note)
  })
  mongoose.connection.close()
})

当代码执行时,程序会输出存储在数据库中的所有便笺:

fullstack content

这些对象是通过 Note 模型的find方法从数据库中检索的。 该方法的参数是表示搜索条件的对象。 因为参数是一个空的对象{},所以我们得到了存储在 notes 集合中的所有便笺。

搜索条件遵循 Mongo 搜索查询语法

我们可以限制我们的搜索,只包括重要的便笺,如下所示:

Note.find({ important: true }).then(result => {
  // ...
})

Backend connected to a database

【后端连接到数据库】

现在我们有足够的知识,可以在我们的应用中使用 Mongo了。

让我们通过复制粘贴 Mongoose 定义到index.js 文件来快速开始:

const mongoose = require('mongoose')

// DO NOT SAVE YOUR PASSWORD TO GITHUB!!
const url =
  'mongodb+srv://fullstack:sekred@cluster0-ostce.mongodb.net/note-app?retryWrites=true'

mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true })

const noteSchema = new mongoose.Schema({
  content: String,
  date: Date,
  important: Boolean,
})

const Note = mongoose.model('Note', noteSchema)

让我们将获取所有便笺的处理程序更改为如下形式:

app.get('/api/notes', (request, response) => {
  Note.find({}).then(notes => {
    response.json(notes)
  })
})

我们可以在浏览器中验证后端是否可以显示所有文档:

fullstack content

这个应用运行得几乎完美。 前端假设每个对象在id 字段中都有唯一的 id。 我们也不想将 mongo 版本控制字段 __v 返回到前端。

格式化 Mongoose 返回的对象的一种方法是修改Schema 的 toJSON 方法,这个schema是作用在所有models实例上的。 修改方法的过程如下:

noteSchema.set('toJSON', {
  transform: (document, returnedObject) => {
    returnedObject.id = returnedObject._id.toString()
    delete returnedObject._id
    delete returnedObject.__v
  }
})

尽管 Mongoose 对象的 id 属性看起来像一个字符串,但实际上它是一个对象。 为了安全起见,我们定义的 toJSON 方法将其转换为字符串。 如果我们不进行这个更改,那么一旦我们开始编写测试,它将在未来对我们造成更大的麻烦。

让我们用一个用 toJSON 方法格式化的对象列表来响应 HTTP 请求:

app.get('/api/notes', (request, response) => {
  Note.find({}).then(notes => {
    response.json(notes)
  })
})

现在,notes 变量被分配给 Mongo 返回的对象数组。 当response 以JSON 格式返回,数组中每个对象 toJSON 方法会通过JSON.stringify自动调用

Database configuration into its own module

【数据库逻辑配置到单独的模块】

在我们重构后端的其余部分来使用数据库之前,让我们将 Mongoose 特定的代码提取到它自己的模块中。

让我们为模块models 创建一个新目录,并添加一个名为note.js 的文件:

const mongoose = require('mongoose')

const url = process.env.MONGODB_URI
console.log('connecting to', url)
mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(result => {    console.log('connected to MongoDB')  })  .catch((error) => {    console.log('error connecting to MongoDB:', error.message)  })
const noteSchema = new mongoose.Schema({
  content: String,
  date: Date,
  important: Boolean,
})

noteSchema.set('toJSON', {
  transform: (document, returnedObject) => {
    returnedObject.id = returnedObject._id.toString()
    delete returnedObject._id
    delete returnedObject.__v
  }
})

module.exports = mongoose.model('Note', noteSchema)

定义 Node modules与第2章节中定义ES6模块的方式稍有不同。

模块的公共接口是通过将值设置为 module.exports 变量来定义的。 我们将该值设置为Note 模型。 模块内部定义的其他东西,比如变量 mongoose 和 url 对于模块的用户来说是不可访问的或者不可见的。

导入模块的方法是在index.js 中添加如下代码行:

const Note = require('./models/note')

这样,Note 变量将被分配给模块定义的同一个对象。

建立链接的方式略有改变:

const url = process.env.MONGODB_URI

console.log('connecting to', url)

mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(result => {
    console.log('connected to MongoDB')
  })
  .catch((error) => {
    console.log('error connecting to MongoDB:', error.message)
  })

将数据库的地址硬编码到代码中并不是一个好主意,因此数据库的地址通过MONGODB_URI 环境变量传递给应用。

建立连接的方法现在被赋予处理成功和失败的连接尝试的函数。 这两个函数都只是向控制台发送一条关于成功状态的消息:

fullstack content

有很多方法可以定义环境变量的值。 一种方法是在应用启动时定义它:

MONGODB_URI=address_here npm run watch

一个更复杂的方法是使用dotenv ,你可以使用如下命令安装库:

npm install dotenv --save

为了使用这个库,我们创建一个 .env 文件在项目的根部。 环境变量是在文件内部定义的,它可以是这样的:

MONGODB_URI='mongodb+srv://fullstack:sekred@cluster0-ostce.mongodb.net/note-app?retryWrites=true'
PORT=3001

我们还将服务器的硬编码端口添加到 PORT 环境变量中。

** .env 文件应立即放到gitignore中,因为我们不希望在网上公开任何机密信息! **

fullstack content

可以使用require('dotenv').config()命令来使用 .env 文件中定义的环境变量。您可以在代码中引用它们,就像引用普通环境变量一样,使用熟悉的 process.env.MONGODB_URI语法。

让我们如下面的方式更改index.js 文件:

require('dotenv').config()const express = require('express')
const app = express()
const Note = require('./models/note')
// ..

const PORT = process.env.PORTapp.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

在导入note 模型之前导入dotenv 非常重要。 这样可以确保在导入其他模块的代码之前, .env 文件是全局可用的。

Using database in route handlers

【在路由处理程序中使用数据库】

接下来,让我们更改后端功能的其余部分来使用数据库。

创建一个新的便笺是这样完成的:

app.post('/api/notes', (request, response) => {
  const body = request.body

  if (body.content === undefined) {
    return response.status(400).json({ error: 'content missing' })
  }

  const note = new Note({
    content: body.content,
    important: body.important || false,
    date: new Date(),
  })

  note.save().then(savedNote => {
    response.json(savedNote)
  })
})

使用 Note 构造函数创建 Note 对象。 请求的响应是在保存操作的回调函数中发送的。 这确保只有在操作成功时才发送响应。 稍后我们将讨论错误处理。

回调函数中的 savedNote 参数是保存的和新创建的便笺。 返回的数据是用 toJSON 方法创建的格式化版本:

response.json(savedNote)

取一个单独的便笺代码改为:

app.get('/api/notes/:id', (request, response) => {
  Note.findById(request.params.id).then(note => {
    response.json(note)
  })
})

Verifying frontend and backend integration

【验证前端和后端的集成】

当后端扩展时,最好先用 **浏览器,Postman 或者 VS Code REST 客户端 **来测试后端。 接下来,让我们尝试在使用数据库之后创建一个新的便笺:

fullstack content

只有当所有的东西都被验证在后端工作良好时,测试前端是否与后端一起工作才是一个好主意。 仅仅通过前端测试是非常低效的。

一次集成一个前端和后端功能可能是个好主意。 首先,我们可以实现从数据库中获取所有便笺,并通过浏览器中的后端端点对其进行测试。 在此之后,我们可以验证前端是否与新的后端一起工作。 一旦一切看起来正常,我们就会进入下一个特性。

一旦我们将数据库混入其中,检查数据库中持久存储的状态就很有用了,例如,通过 MongoDB Atlas 中的控制面板来检查。 很多时候,像我们前面编写的mongo.js 程序这样的小型 Node helper 程序在开发过程中会非常有用。

您可以在this Github repositorypart3-4 分支中找到我们当前应用的全部代码。

Error handling

【错误处理】

如果我们试图向数据库访问一个实际上并不存在的 id 的便笺的 URL,比如 http://localhost:3001/api/notes/5c41c90e84d891c15dfa3431 ,其中5c41c90e84d891c15dfa3431 不是一个存储在数据库中的 id,那么返回值将会是 null

我们来改变一下行为,当给定id的note不存在时,服务端会向请求响应404状态。另外我们来实现一个简单的catch代码块,来处理findById方法返回rejected的情况:

app.get('/api/notes/:id', (request, response) => {
  Note.findById(request.params.id)
    .then(note => {
      if (note) {      response.json(note)      } else {        response.status(404).end()      }
    })
    .catch(error => {      console.log(error)      response.status(500).end()    })})

如果在数据库中没有找到匹配的对象,note 的值会是 null, 于是 else 代码块执行。因此返回的状态是404 not found。而如果 findById 方法的promise 状态会是 500 internal server error。 可以在控制台中看到更多的错误打印信息。

On top of the non-existing note, there's one more error situation needed to be handled. In this situation, we are trying to fetch a note with a wrong kind of id, meaning an id that doesn't match the mongo identifier format. 出了note 不存在的错误,还有许多错误是需要处理的。现在,我们尝试获取一个错误的 id, 也就是一个与mongo 标识符格式不匹配的 id

如果我们提出如下请求,我们将得到如下所示的错误消息:


Method: GET
Path:   /api/notes/someInvalidId
Body:   {}
---
{ CastError: Cast to ObjectId failed for value "someInvalidId" at path "_id"
    at CastError (/Users/mluukkai/opetus/_fullstack/osa3-muisiinpanot/node_modules/mongoose/lib/error/cast.js:27:11)
    at ObjectId.cast (/Users/mluukkai/opetus/_fullstack/osa3-muisiinpanot/node_modules/mongoose/lib/schema/objectid.js:158:13)
    ...

当给出了一个奇怪的id作为参数时,findById 方法会抛出一个错误,进而会导致promise返回了rejected,因此也就会触发catch代码块中的函数。

我们应该区分这两种不同类型的错误情况。 后者实际上是由我们自己的代码引起的错误。

让我们如下面的方式修改代码:

app.get('/api/notes/:id', (request, response) => {
  Note.findById(request.params.id)
    .then(note => {
      if (note) {
        response.json(note)
      } else {
        response.status(404).end() 
      }
    })
    .catch(error => {
      console.log(error)
      response.status(400).send({ error: 'malformatted id' })    })
})

如果 id 的格式不正确,那么我们将在 catch 块中定义的错误处理程序中结束。 适合这种情况的状态代码是 400 Bad Request ,因为这种情况完全符合描述:

The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications. 由于格式不正确的语法,服务器无法理解请求。 客户端不应该在没有修改的情况下重复请求。

我们还在响应中添加了一些数据,以阐明错误的原因。

在处理 Promises 时,添加错误和异常处理几乎总是一个好主意,否则您将发现自己正在处理奇怪的 bug。

打印导致错误处理程序控制台异常的对象绝不是个坏主意:

.catch(error => {
  console.log(error)
  response.status(400).send({ error: 'malformatted id' })
})

调用错误处理程序的原因可能与您预期的完全不同。 如果您将错误记录到控制台,您可以避免冗长和令人沮丧的调试会话。此外,更加现代的部署应用的服务支持一些日志系统,你可以去查看这些日志。如前面提到的,Heroku就是一种。

每次您使用后端处理项目时,关注后端的控制台输出是至关重要的。 如果你在一个小屏幕上工作,只需要在背景中看到输出的一小部分就足够了。 任何错误消息都会引起你的注意,即使控制台远在后台:

fullstack content

Moving error handling into middleware

【将错误处理移入中间件】

我们在代码的其余部分中编写了错误处理程序的代码。 有时这可能是一个合理的解决方案,但在某些情况下,最好在单个位置实现所有错误处理。 如果我们以后想要将与错误相关的数据报告给外部的错误跟踪系统,比如Sentry,那么这么做就特别有用。

让我们更改 /api/notes/:id 路由的处理程序,以便它使用next 函数向下传递错误。 下一个函数作为第三个参数传递给处理程序:

app.get('/api/notes/:id', (request, response, next) => {  Note.findById(request.params.id)
    .then(note => {
      if (note) {
        response.json(note)
      } else {
        response.status(404).end()
      }
    })
    .catch(error => next(error))})

将向前传递的错误作为参数给 next 函数。 如果在没有参数的情况下调用 next,那么执行将简单地转移到下一个路由或中间件上。 如果使用参数调用next 函数,那么执行将继续到error 处理程序中间件

Express error handlers是一种中间件,它定义了一个接受4个参数 的函数。 我们的错误处理程序如下所示:

const errorHandler = (error, request, response, next) => {
  console.error(error.message)

  if (error.name === 'CastError' && error.kind === 'ObjectId') {
    return response.status(400).send({ error: 'malformatted id' })
  } 

  next(error)
}

app.use(errorHandler)

错误处理程序检查错误是否是CastError 异常,在这种情况下,我们知道错误是由 Mongo 的无效对象 id 引起的。 在这种情况下,错误处理程序将向浏览器发送响应,并将response对象作为参数传递。 在所有其他错误情况下,中间件将错误转发给缺省的 Express 错误处理程序。

The order of middleware loading

【中间件加载顺序】

中间件的执行顺序与通过 app.use 函数加载到 express 中的顺序相同。 出于这个原因,在定义中间件时一定要小心。

正确的顺序如下:

app.use(express.static('build'))
app.use(express.json())
app.use(logger)

app.post('/api/notes', (request, response) => {
  const body = request.body
  // ...
})

const unknownEndpoint = (request, response) => {
  response.status(404).send({ error: 'unknown endpoint' })
}

// handler of requests with unknown endpoint
app.use(unknownEndpoint)

const errorHandler = (error, request, response, next) => {
  // ...
}

// handler of requests with result to errors
app.use(errorHandler)

Json-parser 中间件应该是最早加载到 Express 中的中间件之一,如果顺序变成了下面这样:

app.use(logger) // request.body is empty!

app.post('/api/notes', (request, response) => {
  // request.body is empty!
  const body = request.body
  // ...
})

app.use(express.json())

那么,由 HTTP 请求发送的 JSON 数据将不能用于日志记录器中间件或 POST 路由处理程序,因为此时 request.body 将是一个空对象。

同样重要的是,用于处理不支持路由的中间件位于加载到 Express 的最后一个中间件的旁边,就在错误处理程序之前。

例如,下面的加载顺序会导致一个问题:

const unknownEndpoint = (request, response) => {
  response.status(404).send({ error: 'unknown endpoint' })
}

// handler of requests with unknown endpoint
app.use(unknownEndpoint)

app.get('/api/notes', (request, response) => {
  // ...
})

现在,对未知端点的处理先于 HTTP 请求处理程序。 由于未知端点处理程序使用404未知端点 响应所有请求,在未知端点中间件发送响应之后,将不会调用任何路由或中间件。 唯一的例外是错误处理程序,它需要出现在未知的端点处理程序之后的最后一个端点。

Other operations

【其他操作】

让我们为我们的应用添加一些缺失的功能,包括删除和更新单个便笺。

从数据库中删除便笺最简单的方法是使用findByIdAndRemove方法:

app.delete('/api/notes/:id', (request, response, next) => {
  Note.findByIdAndRemove(request.params.id)
    .then(result => {
      response.status(204).end()
    })
    .catch(error => next(error))
})

在删除资源的两个“成功”案例中,后端都使用状态码 204 no content.进行响应。 两种不同的情况是删除已存在的便笺,以及删除数据库中不存在的便笺。 结果回调参数可用于检查资源是否实际被删除,如果认为有必要,我们可以使用该信息为两种情况返回不同的状态代码。 发生的任何异常都会传递到错误处理程序上。

通过findbyidanddupdate方法可以轻松地切换便笺的重要性。

app.put('/api/notes/:id', (request, response, next) => {
  const body = request.body

  const note = {
    content: body.content,
    important: body.important,
  }

  Note.findByIdAndUpdate(request.params.id, note, { new: true })
    .then(updatedNote => {
      response.json(updatedNote)
    })
    .catch(error => next(error))
})

在上面的代码中,我们也允许编辑便笺的内容。 然而,出于显而易见的原因,我们不支持更改创建日期。

注意,findByIdAndUpdate 方法接收一个常规的 JavaScript 对象作为参数,而不是用 Note 构造函数创建的新便笺对象。

关于 findByIdAndUpdate方法的使用有一个重要的细节。 默认情况下,事件处理程序的 updatedNote 参数接收原始文档无需修改。 我们添加了可选的代码{ new: true } 参数,这将导致使用新修改的文档而不是原始文档调用事件处理程序。

在使用 Postman 和 VS Code REST 客户端直接测试后端之后,我们可以验证它似乎可以工作。 前端似乎也与使用数据库的后端一起工作。

当我们切换注意事项的重要性时,我们会在控制台中看到如下令人担忧的错误消息:

fullstack content

谷歌错误信息将导向 instructions 解决问题。 根据 Mongoose 文档中的建议,我们在note.js 文件中添加了如下内容 :

const mongoose = require('mongoose')

mongoose.set('useFindAndModify', false)
// ...
  
module.exports = mongoose.model('Note', noteSchema) 

您可以在this github repository.的part3-5 分支中找到我们当前应用的全部代码。