Express 教程第三部分:使用数据库(Mongoose)
本文简要介绍了数据库以及如何在 Node/Express 应用程序中使用它们。然后继续展示了如何使用 Mongoose 为 LocalLibrary 网站提供数据库访问。它解释了如何声明对象 schema 和模型、主要字段类型以及基本验证。它还简要展示了访问模型数据的几种主要方法。
预备知识 | Express 教程第二部分:创建一个骨架网站 |
---|---|
目标 | 能够使用 Mongoose 设计和创建自己的模型。 |
概述
图书馆工作人员将使用本地图书馆网站存储有关书籍和借阅者的信息,而图书馆会员将使用它浏览和搜索书籍,了解是否有可用副本,然后预订或借阅它们。为了高效地存储和检索信息,我们将把它们存储在数据库中。
Express 应用程序可以使用许多不同的数据库,并且您可以使用几种方法来执行Create(创建)、Read(读取)、Update(更新)和Delete(删除)(CRUD) 操作。本教程简要概述了一些可用选项,然后详细介绍了所选的特定机制。
我可以使用哪些数据库?
Express 应用程序可以使用 Node 支持的任何数据库(Express 本身不定义任何特定的数据库管理附加行为/要求)。有许多流行选项,包括 PostgreSQL、MySQL、Redis、SQLite 和 MongoDB。
在选择数据库时,您应该考虑诸如生产力/学习曲线时间、性能、复制/备份的便捷性、成本、社区支持等因素。虽然没有单一的“最佳”数据库,但对于我们 Local Library 这样的小型到中型网站来说,几乎所有流行的解决方案都应该足够。
有关选项的更多信息,请参阅数据库集成 (Express 文档)。
与数据库交互的最佳方式是什么?
与数据库交互有两种常见方法
- 使用数据库的原生查询语言,例如 SQL。
- 使用对象关系映射器(“ORM”)或对象文档映射器(“ODM”)。它们将网站数据表示为 JavaScript 对象,然后将其映射到底层数据库。一些 ORM 和 ODM 与特定数据库绑定,而另一些则提供与数据库无关的后端。
使用 SQL 或数据库支持的任何查询语言可以获得最佳性能。对象映射器通常较慢,因为它们使用转换代码在对象和数据库格式之间进行映射,这可能无法使用最有效的数据库查询(如果映射器支持不同的数据库后端,并且必须在支持的数据库功能方面做出更大的妥协,则尤其如此)。
使用 ORM/ODM 的好处是,程序员可以继续以 JavaScript 对象的术语而不是数据库语义进行思考——如果您需要使用不同的数据库(在相同或不同的网站上),这一点尤其如此。它们还提供了一个执行数据验证的明确位置。
注意: 使用 ODM/ORM 通常会降低开发和维护成本!除非您非常熟悉原生查询语言或性能至关重要,否则您应该强烈考虑使用 ODM。
我应该使用哪个 ORM/ODM?
npm 包管理器网站上提供了许多 ODM/ORM 解决方案(查看 odm 和 orm 标签以获取子集!)。
在撰写本文时流行的一些解决方案是
- Mongoose:Mongoose 是一个 MongoDB 对象建模工具,旨在异步环境中工作。
- Waterline:一个从基于 Express 的 Sails Web 框架中提取的 ORM。它提供了统一的 API 来访问许多不同的数据库,包括 Redis、MySQL、LDAP、MongoDB 和 Postgres。
- Bookshelf:具有基于 Promise 和传统回调接口,提供事务支持、急切/嵌套急切关系加载、多态关联以及对一对一、一对多和多对多关系的支持。适用于 PostgreSQL、MySQL 和 SQLite3。
- Objection:尽可能简化 SQL 和底层数据库引擎的全部功能的使用(支持 SQLite3、Postgres 和 MySQL)。
- Sequelize 是一个基于 Promise 的 Node.js 和 io.js ORM。它支持 PostgreSQL、MySQL、MariaDB、SQLite 和 MSSQL 方言,并具有强大的事务支持、关系、读复制等功能。
- Node ORM2 是 NodeJS 的对象关系管理器。它支持 MySQL、SQLite 和 Postgres,有助于使用面向对象的方法处理数据库。
- GraphQL:主要是一种用于 RESTful API 的查询语言,GraphQL 非常流行,并且具有从数据库读取数据的功能。
通常,在选择解决方案时,您应该同时考虑所提供的功能和“社区活动”(下载量、贡献量、错误报告、文档质量等)。在撰写本文时,Mongoose 是迄今为止最流行的 ODM,如果您正在将 MongoDB 用作数据库,它是一个合理的选择。
将 Mongoose 和 MongoDB 用于 LocalLibrary
对于 Local Library 示例(以及本主题的其余部分),我们将使用 Mongoose ODM 来访问我们的图书馆数据。Mongoose 作为 MongoDB 的前端,MongoDB 是一个开源的 NoSQL 数据库,它使用面向文档的数据模型。MongoDB 数据库中的“文档集合”类似于关系数据库中的“表的行”。
这种 ODM 和数据库组合在 Node 社区中非常流行,部分原因是文档存储和查询系统看起来非常像 JSON,因此 JavaScript 开发人员很熟悉。
注意: 您不需要了解 MongoDB 才能使用 Mongoose,尽管如果您已经熟悉 MongoDB,Mongoose 文档的某些部分会更容易使用和理解。
本教程的其余部分展示了如何为 LocalLibrary 网站示例定义和访问 Mongoose schema 和模型。
设计 LocalLibrary 模型
在您开始编写模型代码之前,花几分钟时间思考我们需要存储哪些数据以及不同对象之间的关系是值得的。
我们知道我们需要存储书籍信息(标题、摘要、作者、类型、ISBN),并且我们可能有多个可用副本(具有全局唯一 ID、可用性状态等)。我们可能需要存储比作者姓名更多的作者信息,并且可能存在多个姓名相同或相似的作者。我们希望能够根据书名、作者、类型和类别对信息进行排序。
在设计模型时,为每个“对象”(一组相关信息)创建单独的模型是有意义的。在这种情况下,这些模型的一些明显候选者是书籍、书籍实例和作者。
您可能还希望使用模型来表示选择列表选项(例如,像下拉选择列表),而不是将选择硬编码到网站本身——当所有选项未提前知晓或可能更改时,建议这样做。一个很好的例子是类型(例如,奇幻、科幻等)。
一旦我们确定了模型和字段,我们需要考虑它们之间的关系。
考虑到这一点,下面的 UML 关联图显示了我们将在此案例中定义的模型(用方框表示)。如上所述,我们为书籍(书籍的通用详细信息)、书籍实例(系统中可用书籍特定物理副本的状态)和作者创建了模型。我们还决定为类型创建一个模型,以便可以动态创建值。我们决定不为 BookInstance:status
创建模型——我们将硬编码可接受的值,因为我们不期望这些值会改变。在每个方框中,您可以看到模型名称、字段名称和类型,以及方法及其返回类型。
该图还显示了模型之间的关系,包括它们的多重性。多重性是图上的数字,显示关系中可能存在的每个模型的数量(最大值和最小值)。例如,方框之间的连接线显示 Book
和 Genre
是相关的。靠近 Book
模型的数字显示一个 Genre
必须有零个或多个 Book
(您可以根据需要拥有任意数量),而线另一端靠近 Genre
的数字显示一本书可以有零个或多个关联的 Genre
。
注意: 如我们下面 Mongoose 简介 中所讨论,通常最好将定义文档/模型之间关系的字段放在一个模型中(您仍然可以通过在另一个模型中搜索关联的 _id
来找到反向关系)。下面我们选择在 Book schema 中定义 Book
/Genre
和 Book
/Author
之间的关系,在 BookInstance
Schema 中定义 Book
/BookInstance
之间的关系。这个选择有些随意——我们也可以将字段放在另一个 schema 中。
注意: 下一节提供了一个基本简介,解释了如何定义和使用模型。阅读时,请考虑我们将如何构建上面图中的每个模型。
数据库 API 是异步的
用于创建、查找、更新或删除记录的数据库方法是异步的。这意味着这些方法会立即返回,而处理方法成功或失败的代码将在操作完成后的某个时间运行。当服务器等待数据库操作完成时,其他代码可以执行,因此服务器可以保持对其他请求的响应。
JavaScript 有许多支持异步行为的机制。历史上,JavaScript 严重依赖将回调函数传递给异步方法来处理成功和错误情况。在现代 JavaScript 中,回调已基本被Promise取代。Promise 是异步方法(立即)返回的对象,表示其未来状态。当操作完成时,Promise 对象“已解决”,并解析一个表示操作结果或错误的对象。
当 Promise 解决时,有两种主要方法可以使用 Promise 来运行代码,我们强烈建议您阅读如何使用 Promise 以获取两种方法的高级概述。在本教程中,我们将主要使用await
在async function
中等待 Promise 完成,因为这会使异步代码更具可读性和可理解性。
这种方法的原理是,您使用 async function
关键字将函数标记为异步函数,然后在该函数内部将 await
应用于任何返回 Promise 的方法。当异步函数执行时,其操作在第一个 await
方法处暂停,直到 Promise 解决。从周围代码的角度来看,异步函数然后返回,并且其后的代码能够运行。稍后当 Promise 解决时,异步函数内部的 await
方法返回结果,如果 Promise 被拒绝,则会抛出错误。然后,异步函数中的代码将执行,直到遇到另一个 await
(此时它将再次暂停),或者直到函数中的所有代码都已运行。
您可以在下面的示例中看到它的工作原理。myFunction()
是一个在try...catch
块中调用的异步函数。当 myFunction()
运行时,代码执行在 methodThatReturnsPromise()
处暂停,直到 Promise 解决,此时代码继续执行到 functionThatReturnsPromise()
并再次等待。如果在异步函数中抛出错误,catch
块中的代码将运行,如果任何方法返回的 Promise 被拒绝,就会发生这种情况。
async function myFunction() {
// …
await someObject.methodThatReturnsPromise();
// …
await functionThatReturnsPromise();
// …
}
try {
// …
myFunction();
// …
} catch (e) {
// error handling code
}
上面的异步方法按顺序运行。如果方法之间不相互依赖,则可以并行运行它们,从而更快地完成整个操作。这通过使用 Promise.all()
方法来完成,该方法将可迭代的 Promise 作为输入并返回单个 Promise
。当所有输入的 Promise 都完成时,此返回的 Promise 将完成,并带有一个包含完成值的数组。当任何输入的 Promise 被拒绝时,它将拒绝,并带上第一个拒绝原因。
下面的代码演示了其工作原理。首先,我们有两个返回 Promise 的函数。我们 await
它们通过 Promise.all()
返回的 Promise 完成。一旦它们都完成,await
返回并填充结果数组,函数然后继续到下一个 await
,并等待 anotherFunctionThatReturnsPromise()
返回的 Promise 解决。您将在 try...catch
块中调用 myFunction()
以捕获任何错误。
async function myFunction() {
// …
const [resultFunction1, resultFunction2] = await Promise.all([
functionThatReturnsPromise1(),
functionThatReturnsPromise2(),
]);
// …
await anotherFunctionThatReturnsPromise(resultFunction1);
}
带有 await
/async
的 Promise 允许对异步执行进行灵活且“可理解”的控制!
Mongoose 简介
本节概述了如何将 Mongoose 连接到 MongoDB 数据库,如何定义 schema 和模型,以及如何进行基本查询。
注意: 本简介深受 npm 上的 Mongoose 快速入门 和 官方文档 的影响。
安装 Mongoose 和 MongoDB
Mongoose 像任何其他依赖项一样安装在您的项目 (package.json) 中——使用 npm。要在项目文件夹中安装它,请使用以下命令
npm install mongoose
安装 Mongoose 会添加其所有依赖项,包括 MongoDB 数据库驱动程序,但它不会安装 MongoDB 本身。如果您想安装 MongoDB 服务器,则可以在此处为各种操作系统下载安装程序并在本地安装。您也可以使用基于云的 MongoDB 实例。
注意: 对于本教程,我们将使用 MongoDB Atlas 云托管的数据库即服务免费层来提供数据库。这适用于开发,并且对于本教程来说是合理的,因为它使“安装”与操作系统无关(数据库即服务也是您可能用于生产数据库的一种方法)。
连接到 MongoDB
Mongoose 需要连接到 MongoDB 数据库。您可以 require()
并使用 mongoose.connect()
连接到本地托管的数据库,如下所示(对于本教程,我们将改为连接到互联网托管的数据库)。
// Import the mongoose module
const mongoose = require("mongoose");
// Set `strictQuery: false` to globally opt into filtering by properties that aren't in the schema
// Included because it removes preparatory warnings for Mongoose 7.
// See: https://mongoose.node.org.cn/docs/migrating_to_6.html#strictquery-is-removed-and-replaced-by-strict
mongoose.set("strictQuery", false);
// Define the database URL to connect to.
const mongoDB = "mongodb://127.0.0.1/my_database";
// Wait for database to connect, logging an error if there is a problem
main().catch((err) => console.log(err));
async function main() {
await mongoose.connect(mongoDB);
}
注意: 如 数据库 API 是异步的 部分所述,这里我们在 async
函数中 await
connect()
方法返回的 Promise。我们使用 Promise 的 catch()
处理程序来处理连接时可能发生的任何错误,但我们也可以在 try...catch
块中调用 main()
。
您可以通过 mongoose.connection
获取默认的 Connection
对象。如果您需要创建额外的连接,可以使用 mongoose.createConnection()
。它采用与 connect()
相同形式的数据库 URI(包含主机、数据库、端口、选项等)并返回一个 Connection
对象)。请注意,createConnection()
会立即返回;如果您需要等待连接建立,可以调用 asPromise()
来返回一个 Promise(mongoose.createConnection(mongoDB).asPromise()
)。
定义和创建模型
模型使用 Schema
接口定义。Schema 允许您定义存储在每个文档中的字段及其验证要求和默认值。此外,您可以定义静态和实例辅助方法,以便更轻松地处理数据类型,以及可以像任何其他字段一样使用的虚拟属性,但它们实际上并不存储在数据库中(我们将在下面进一步讨论)。
然后使用 mongoose.model()
方法将 Schema “编译”成模型。一旦有了模型,您就可以使用它来查找、创建、更新和删除给定类型的对象。
注意: 每个模型都映射到 MongoDB 数据库中的文档集合。文档将包含模型 Schema
中定义的字段/Schema 类型。
定义 Schema
下面的代码片段展示了如何定义一个简单的 schema。首先,您 require()
mongoose,然后使用 Schema 构造函数创建一个新的 schema 实例,并在构造函数的对象参数中定义各种字段。
// Require Mongoose
const mongoose = require("mongoose");
// Define a schema
const Schema = mongoose.Schema;
const SomeModelSchema = new Schema({
a_string: String,
a_date: Date,
});
在上面的例子中,我们只有两个字段,一个字符串和一个日期。在下一节中,我们将展示一些其他字段类型、验证和其他方法。
创建模型
模型是使用 mongoose.model()
方法从 Schema 创建的
// Define schema
const Schema = mongoose.Schema;
const SomeModelSchema = new Schema({
a_string: String,
a_date: Date,
});
// Compile model from schema
const SomeModel = mongoose.model("SomeModel", SomeModelSchema);
第一个参数是为模型创建的集合的单数名称(Mongoose 将为上面SomeModel模型创建数据库集合),第二个参数是您希望在创建模型时使用的 Schema。
注意: 一旦您定义了模型类,您就可以使用它们来创建、更新或删除记录,并运行查询以获取所有记录或特定记录子集。我们将在使用模型部分以及创建视图时向您展示如何执行此操作。
Schema 类型(字段)
一个 Schema 可以有任意数量的字段——每个字段代表 MongoDB 中存储的文档中的一个字段。下面显示了一个示例 Schema,展示了许多常见的字段类型以及它们的声明方式。
const schema = new Schema({
name: String,
binary: Buffer,
living: Boolean,
updated: { type: Date, default: Date.now() },
age: { type: Number, min: 18, max: 65, required: true },
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
array: [],
ofString: [String], // You can also have an array of each of the other types too.
nested: { stuff: { type: String, lowercase: true, trim: true } },
});
大多数 SchemaTypes(“type:”或字段名称之后的描述符)都是不言自明的。例外情况是
ObjectId
:表示数据库中模型的特定实例。例如,一本书可以使用它来表示其作者对象。这实际上将包含指定对象的唯一 ID (_id
)。我们可以在需要时使用populate()
方法来引入关联信息。Mixed
:任意 schema 类型。[]
:项目数组。您可以在这些模型上执行 JavaScript 数组操作(push、pop、unshift 等)。上面的示例显示了一个没有指定类型的对象数组和一个String
对象数组,但您可以拥有任何类型的对象数组。
代码还显示了两种声明字段的方式
- 字段名称和类型作为键值对(即,像字段
name
、binary
和living
那样)。 - 字段名称后跟一个对象,该对象定义了字段的
type
和任何其他选项。选项包括:- 默认值。
- 内置验证器(例如,最大/最小值)和自定义验证函数。
- 该字段是否为必需字段
String
字段是否应自动设置为小写、大写或修剪(例如,{ type: String, lowercase: true, trim: true }
)
有关选项的更多信息,请参阅 SchemaTypes (Mongoose 文档)。
验证
Mongoose 提供内置和自定义验证器,以及同步和异步验证器。它允许您在所有情况下指定可接受的值范围和验证失败的错误消息。
内置验证器包括
以下示例(略微修改自 Mongoose 文档)展示了如何指定某些验证器类型和错误消息
const breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, "Too few eggs"],
max: 12,
required: [true, "Why no eggs?"],
},
drink: {
type: String,
enum: ["Coffee", "Tea", "Water"],
},
});
有关字段验证的完整信息,请参阅 验证 (Mongoose 文档)。
虚拟属性
虚拟属性是您可以获取和设置的文档属性,但它们不会持久化到 MongoDB。getter 对于格式化或组合字段很有用,而 setter 对于将单个值分解为多个值以进行存储很有用。文档中的示例从名字和姓氏字段构建(和解构)一个全名虚拟属性,这比每次在模板中使用时都构建全名更容易、更简洁。
注意: 我们将在库中使用一个虚拟属性,通过路径和记录的 _id
值来为每个模型记录定义一个唯一的 URL。
有关更多信息,请参阅 虚拟属性 (Mongoose 文档)。
方法和查询助手
一个 schema 还可以有实例方法、静态方法和查询助手。实例方法和静态方法类似,但显而易见的区别是实例方法与特定记录相关联并可以访问当前对象。查询助手允许您扩展 mongoose 的链式查询构建器 API(例如,除了 find()
、findOne()
和 findById()
方法之外,还可以添加查询“byName”)。
使用模型
一旦创建了一个 schema,就可以使用它来创建模型。模型代表数据库中可搜索的文档集合,而模型的实例代表您可以保存和检索的单个文档。
我们将在下面提供一个简要概述。有关更多信息,请参阅:模型 (Mongoose 文档)。
注意: 记录的创建、更新、删除和查询都是异步操作,会返回一个 Promise。下面的示例仅展示了相关方法的用法和 await
(即,使用方法的核心代码)。为了清晰起见,省略了用于捕获错误的外部 async function
和 try...catch
块。有关使用 await/async
的更多信息,请参阅上面的 数据库 API 是异步的。
创建和修改文档
要创建记录,您可以定义模型的实例,然后对其调用 save()
。下面的示例假设 SomeModel
是我们从 schema 创建的模型(带有一个字段 name
)。
// Create an instance of model SomeModel
const awesome_instance = new SomeModel({ name: "awesome" });
// Save the new model instance asynchronously
await awesome_instance.save();
您还可以使用 create()
在保存模型实例的同时定义它。下面我们只创建一个,但您可以通过传入对象数组来创建多个实例。
await SomeModel.create({ name: "also_awesome" });
每个模型都有一个关联的连接(当您使用 mongoose.model()
时,这将是默认连接)。您可以创建一个新连接并在其上调用 .model()
以在不同的数据库上创建文档。
您可以使用点语法访问此新记录中的字段,并更改值。您必须调用 save()
或 update()
才能将修改后的值存储回数据库。
// Access model field values using dot notation
console.log(awesome_instance.name); // should log 'also_awesome'
// Change record by modifying the fields, then calling save().
awesome_instance.name = "New cool name";
await awesome_instance.save();
搜索记录
您可以使用查询方法搜索记录,将查询条件指定为 JSON 文档。下面的代码片段展示了如何查找数据库中所有打网球的运动员,只返回运动员的姓名和年龄字段。这里我们只指定一个匹配字段(运动),但您可以添加更多条件,指定正则表达式条件,或者完全删除条件以返回所有运动员。
const Athlete = mongoose.model("Athlete", yourSchema);
// find all athletes who play tennis, returning the 'name' and 'age' fields
const tennisPlayers = await Athlete.find(
{ sport: "Tennis" },
"name age",
).exec();
注意: 重要的是要记住,未找到任何结果对于搜索来说不是错误——但在应用程序的上下文中可能是一个失败情况。如果您的应用程序期望搜索能够找到值,您可以检查结果中返回的条目数量。
查询 API,例如 find()
,返回类型为 Query 的变量。您可以使用查询对象在执行 exec()
方法之前分部分构建查询。exec()
执行查询并返回一个 Promise,您可以 await
该 Promise 以获取结果。
// find all athletes that play tennis
const query = Athlete.find({ sport: "Tennis" });
// selecting the 'name' and 'age' fields
query.select("name age");
// limit our results to 5 items
query.limit(5);
// sort by age
query.sort({ age: -1 });
// execute the query at a later time
query.exec();
上面我们已经在 find()
方法中定义了查询条件。我们也可以使用 where()
函数来实现这一点,并且我们可以使用点运算符 (.) 将查询的所有部分链接在一起,而不是单独添加它们。下面的代码片段与我们上面的查询相同,并额外添加了年龄条件。
Athlete.find()
.where("sport")
.equals("Tennis")
.where("age")
.gt(17)
.lt(50) // Additional where query
.limit(5)
.sort({ age: -1 })
.select("name age")
.exec();
find()
方法获取所有匹配的记录,但通常您只想获取一个匹配。以下方法查询单个记录
findById()
:查找具有指定id
的文档(每个文档都有唯一的id
)。findOne()
:查找匹配指定条件的单个文档。findByIdAndDelete()
、findByIdAndUpdate()
、findOneAndRemove()
、findOneAndUpdate()
:通过id
或条件查找单个文档,并对其进行更新或删除。这些是用于更新和删除记录的便捷函数。
注意: 还有一个 countDocuments()
方法,您可以使用它来获取匹配条件的项数。如果您想在不实际获取记录的情况下执行计数,这很有用。
查询还有很多其他功能。有关更多信息,请参阅:查询 (Mongoose 文档)。
使用相关文档 — 填充
您可以使用 ObjectId
schema 字段从一个文档/模型实例创建对另一个文档/模型实例的引用,或使用 ObjectId
数组从一个文档创建对多个文档的引用。该字段存储相关模型的 ID。如果需要关联文档的实际内容,可以在查询中使用 populate()
方法将 ID 替换为实际数据。
例如,下面的 schema 定义了作者和故事。每个作者可以有多个故事,我们将其表示为 ObjectId
数组。每个故事可以有一个作者。ref
属性告诉 schema 哪个模型可以分配给此字段。
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const authorSchema = new Schema({
name: String,
stories: [{ type: Schema.Types.ObjectId, ref: "Story" }],
});
const storySchema = new Schema({
author: { type: Schema.Types.ObjectId, ref: "Author" },
title: String,
});
const Story = mongoose.model("Story", storySchema);
const Author = mongoose.model("Author", authorSchema);
我们可以通过分配 _id
值来保存对相关文档的引用。下面我们创建一个作者,然后是一个故事,并将作者 ID 分配给故事的作者字段。
const bob = new Author({ name: "Bob Smith" });
await bob.save();
// Bob now exists, so lets create a story
const story = new Story({
title: "Bob goes sledding",
author: bob._id, // assign the _id from our author Bob. This ID is created by default!
});
await story.save();
注意: 这种编程风格的一个巨大好处是,我们不必用错误检查来使代码的主路径复杂化。如果任何 save()
操作失败,Promise 将被拒绝并抛出错误。我们的错误处理代码单独处理这个问题(通常在 catch()
块中),因此我们代码的意图非常清晰。
我们的故事文档现在有一个由作者文档 ID 引用的作者。为了在故事结果中获取作者信息,我们使用 populate()
,如下所示。
Story.findOne({ title: "Bob goes sledding" })
.populate("author") // Replace the author id with actual author information in results
.exec();
注意: 敏锐的读者会注意到我们给故事添加了一位作者,但我们没有做任何事情将我们的故事添加到作者的 stories
数组中。那么我们如何才能获取特定作者的所有故事呢?一种方法是将我们的故事添加到 stories 数组中,但这会导致我们在两个地方维护作者和故事相关信息。
更好的方法是获取我们作者的 _id
,然后使用 find()
在所有故事的作者字段中搜索它。
Story.find({ author: bob._id }).exec();
这几乎是您在本教程中需要了解的有关处理相关项目的所有信息。有关更详细的信息,请参阅 填充 (Mongoose 文档)。
每个文件一个 schema/模型
虽然您可以使用您喜欢的任何文件结构创建 schema 和模型,但我们强烈建议在自己的模块(文件)中定义每个模型 schema,然后导出创建模型的方法。如下所示
// File: ./models/some-model.js
// Require Mongoose
const mongoose = require("mongoose");
// Define a schema
const Schema = mongoose.Schema;
const SomeModelSchema = new Schema({
a_string: String,
a_date: Date,
});
// Export function to create "SomeModel" model class
module.exports = mongoose.model("SomeModel", SomeModelSchema);
然后,您可以在其他文件中立即引用并使用该模型。下面我们展示了如何使用它来获取模型的所有实例。
// Create a SomeModel model just by requiring the module
const SomeModel = require("../models/some-model");
// Use the SomeModel object (model) to find all SomeModel records
const modelInstances = await SomeModel.find().exec();
设置 MongoDB 数据库
现在我们了解了 Mongoose 能做什么以及我们如何设计模型,是时候开始开发 LocalLibrary 网站了。我们首先要做的是设置一个 MongoDB 数据库,用于存储我们的图书馆数据。
对于本教程,我们将使用 MongoDB Atlas 云托管的沙盒数据库。此数据库层不适合生产网站,因为它没有冗余,但非常适合开发和原型设计。我们在此处使用它,因为它免费且易于设置,并且因为 MongoDB Atlas 是一种流行的数据库即服务供应商,您可能会合理地选择它作为生产数据库(在撰写本文时,其他流行的选择包括 ScaleGrid 和 ObjectRocket)。
注意: 如果您愿意,可以通过下载并安装适用于您系统的相应二进制文件来在本地设置 MongoDB 数据库。本文其余说明将类似,只是连接时指定的数据库 URL 会有所不同。在Express 教程第 7 部分:部署到生产环境教程中,我们同时将应用程序和数据库托管在Railway上,但我们同样可以使用MongoDB Atlas上的数据库。
您首先需要创建一个 MongoDB Atlas 账户(这是免费的,只需您填写基本的联系方式并同意其服务条款)。
登录后,您将被带到主页
-
单击概览部分的+ 创建按钮。
-
这将打开部署集群屏幕。单击M0 FREE选项模板。
-
向下滚动页面以查看您可以选择的不同选项。
- 您可以在集群名称下更改集群的名称。在本教程中,我们将其保留为
Cluster0
。 - 取消选择预加载示例数据集复选框,因为我们稍后会导入自己的示例数据。
- 从提供商和区域部分选择任何提供商和区域。不同的区域提供不同的提供商。
- 标签是可选的。我们在这里不使用它们。
- 单击创建部署按钮(集群创建需要几分钟)。
- 您可以在集群名称下更改集群的名称。在本教程中,我们将其保留为
-
这将打开安全快速入门部分。
-
输入您的应用程序用于访问数据库的用户名和密码(上面我们创建了一个新登录名“cooluser”)。请记住安全地复制和存储凭据,因为我们稍后会用到它们。单击创建用户按钮。
注意: 避免在 MongoDB 用户密码中使用特殊字符,因为 mongoose 可能无法正确解析连接字符串。
-
选择按当前 IP 地址添加以允许从您当前的计算机访问
-
在 IP 地址字段中输入
0.0.0.0/0
,然后单击添加条目按钮。这告诉 MongoDB 我们希望允许从任何地方访问。注意: 最佳实践是限制可以连接到数据库和其他资源的 IP 地址。在这里,我们允许从任何地方连接,因为我们不知道部署后请求将来自何处。
-
单击完成并关闭按钮。
-
-
这将打开以下屏幕。单击转到概览按钮。
-
您将返回到概览屏幕。单击左侧部署菜单下的数据库部分。单击浏览集合按钮。
-
这将打开集合部分。单击添加我自己的数据按钮。
-
这将打开创建数据库屏幕。
- 将新数据库的名称输入为
local_library
。 - 将集合名称输入为
Collection0
。 - 单击创建按钮以创建数据库。
- 将新数据库的名称输入为
-
您将返回到集合屏幕,并已创建了数据库。
- 单击概览选项卡返回到集群概览。
-
在 Cluster0 的概览屏幕上,单击连接按钮。
-
这将打开连接到 Cluster0屏幕。
- 选择您的数据库用户。
- 选择驱动程序类别,然后选择驱动程序 Node.js 和版本,如图所示。
- 请勿安装建议的驱动程序。
- 单击复制图标以复制连接字符串。
- 将其粘贴到您的本地文本编辑器中。
- 将连接字符串中的
<password>
占位符替换为您的用户密码。 - 在选项之前,将数据库名称“local_library”插入到路径中(
...mongodb.net/local_library?retryWrites...
) - 将包含此字符串的文件安全地保存起来。
您现在已经创建了数据库,并拥有一个可用于访问它的 URL(包含用户名和密码)。它将类似于:mongodb+srv://your_user_name:your_password@cluster0.cojoign.mongodb.net/local_library?retryWrites=true&w=majority&appName=Cluster0
安装 Mongoose
打开命令提示符并导航到您创建骨架本地图书馆网站的目录。输入以下命令以安装 Mongoose(及其依赖项)并将其添加到您的package.json文件中,除非您在阅读上面的Mongoose 入门时已经完成。
npm install mongoose
连接到 MongoDB
打开 bin/www(从您的项目根目录)并将以下文本复制到您设置端口的位置(在 app.set("port", port);
行之后)。将数据库 URL 字符串(“insert_your_database_url_here”)替换为代表您自己数据库的位置 URL(即,使用来自 MongoDB Atlas 的信息)。
// Set up mongoose connection
const mongoose = require("mongoose");
mongoose.set("strictQuery", false);
const mongoDB = "insert_your_database_url_here";
async function connectMongoose() {
await mongoose.connect(mongoDB);
}
try {
connectMongoose();
} catch (err) {
console.error("Failed to connect to MongoDB:", err);
process.exit(1);
}
如上面 Mongoose 简介 中所讨论,此代码创建到数据库的默认连接并将任何错误报告到控制台。
注意: 我们本可以将数据库连接代码放在 app.js 代码中。将其放在应用程序入口点解耦了应用程序和数据库,这使得为运行测试代码使用不同的数据库变得更容易。
请注意,不建议像上面所示那样将数据库凭据硬编码到源代码中。我们在这里这样做是因为它显示了核心连接代码,并且在开发过程中,泄露这些详细信息不会暴露或损坏敏感信息,风险不大。我们将在部署到生产环境时向您展示如何更安全地执行此操作!
定义 LocalLibrary Schema
我们将为每个模型定义一个单独的模块,如上文所述。首先在项目根目录 (/models) 中创建一个用于存放模型的文件夹,然后为每个模型创建单独的文件
/express-locallibrary-tutorial # the project root /models author.js book.js bookinstance.js genre.js
作者模型
复制下面显示的 Author
schema 代码并将其粘贴到您的 ./models/author.js 文件中。该 schema 定义作者具有名字和姓氏的 String
SchemaType(必填,最大长度为 100 个字符),以及出生日期和死亡日期的 Date
字段。
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const AuthorSchema = new Schema({
first_name: { type: String, required: true, maxLength: 100 },
family_name: { type: String, required: true, maxLength: 100 },
date_of_birth: { type: Date },
date_of_death: { type: Date },
});
// Virtual for author's full name
AuthorSchema.virtual("name").get(function () {
// To avoid errors in cases where an author does not have either a family name or first name
// We want to make sure we handle the exception by returning an empty string for that case
let fullname = "";
if (this.first_name && this.family_name) {
fullname = `${this.family_name}, ${this.first_name}`;
}
return fullname;
});
// Virtual for author's URL
AuthorSchema.virtual("url").get(function () {
// We don't use an arrow function as we'll need the this object
return `/catalog/author/${this._id}`;
});
// Export model
module.exports = mongoose.model("Author", AuthorSchema);
我们还为 AuthorSchema 声明了一个名为“url”的虚拟属性,它返回获取特定模型实例所需的绝对 URL——我们将在模板中需要获取特定作者链接时使用该属性。
注意: 将我们的 URL 声明为 schema 中的虚拟属性是一个好主意,因为这样项目的 URL 只需在一个地方更改。此时,使用此 URL 的链接将无法工作,因为我们还没有任何处理单个模型实例的路由代码。我们将在后面的文章中设置这些!
在模块的末尾,我们导出模型。
书籍模型
最后,复制下面显示的 Book
schema 代码并将其粘贴到您的 ./models/book.js 文件中。大部分与作者模型类似——我们声明了一个包含多个字符串字段的 schema 和一个用于获取特定书籍记录 URL 的虚拟属性,并且我们导出了模型。
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const BookSchema = new Schema({
title: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: "Author", required: true },
summary: { type: String, required: true },
isbn: { type: String, required: true },
genre: [{ type: Schema.Types.ObjectId, ref: "Genre" }],
});
// Virtual for book's URL
BookSchema.virtual("url").get(function () {
// We don't use an arrow function as we'll need the this object
return `/catalog/book/${this._id}`;
});
// Export model
module.exports = mongoose.model("Book", BookSchema);
这里的主要区别在于我们创建了两个对其他模型的引用
- 作者是对单个
Author
模型对象的引用,并且是必需的。 - 类型是对
Genre
模型对象数组的引用。我们尚未声明此对象!
BookInstance 模型
最后,复制下面显示的 BookInstance
schema 代码并将其粘贴到您的 ./models/bookinstance.js 文件中。BookInstance
代表一本书的特定副本,某人可能会借阅,并包含有关副本是否可用、预期归还日期和“印记”(或版本)详细信息的信息。
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const BookInstanceSchema = new Schema({
book: { type: Schema.Types.ObjectId, ref: "Book", required: true }, // reference to the associated book
imprint: { type: String, required: true },
status: {
type: String,
required: true,
enum: ["Available", "Maintenance", "Loaned", "Reserved"],
default: "Maintenance",
},
due_back: { type: Date, default: Date.now },
});
// Virtual for bookinstance's URL
BookInstanceSchema.virtual("url").get(function () {
// We don't use an arrow function as we'll need the this object
return `/catalog/bookinstance/${this._id}`;
});
// Export model
module.exports = mongoose.model("BookInstance", BookInstanceSchema);
我们在这里展示的新内容是字段选项
enum
:这允许我们设置字符串的允许值。在这种情况下,我们用它来指定我们书籍的可用性状态(使用 enum 意味着我们可以防止状态出现拼写错误和任意值)。default
:我们使用 default 将新创建的书籍实例的默认状态设置为“Maintenance”,将默认的due_back
日期设置为now
(请注意如何设置日期时调用 Date 函数!)。
其他一切都应与我们以前的 schema 相同。
类型模型 - 挑战
打开您的 ./models/genre.js 文件并创建一个 schema,用于存储类型(书籍的类别,例如,它是小说还是非小说,浪漫还是军事历史等)。
该定义将与其它模型非常相似
- 该模型应具有一个名为
name
的String
SchemaType,用于描述类型。 - 此名称应为必填项,字符长度在 3 到 100 之间。
- 声明一个名为
url
的类型 URL 的虚拟属性。 - 导出模型。
测试 — 创建一些项目
就这样。我们现在已经设置了网站的所有模型!
为了测试模型(并创建一些我们可以在下一篇文章中使用的示例书籍和其他项目),我们现在将运行一个独立脚本来创建每种类型的项目
-
将文件 populatedb.js 下载(或以其他方式创建)到您的 express-locallibrary-tutorial 目录中(与
package.json
位于同一级别)。注意:
populatedb.js
中的代码可能有助于学习 JavaScript,但理解它对于本教程来说不是必需的。 -
在命令提示符下使用 node 运行脚本,传入您的 MongoDB 数据库的 URL(与您之前在
app.js
中替换 insert_your_database_url_here 占位符的 URL 相同)bashnode populatedb <your MongoDB url>
注意: 在 Windows 上,您需要将数据库 URL 用双引号 (") 括起来。在其他操作系统上,您可能需要单引号 (')。
-
脚本应运行完成,并在终端中显示它创建的项目。
注意: 转到您在 MongoDB Atlas 上的数据库(在集合选项卡中)。您现在应该能够深入到书籍、作者、类型和书籍实例的各个集合中,并查看单个文档。
总结
在本文中,我们学习了一些关于 Node/Express 上的数据库和 ORM 的知识,以及很多关于 Mongoose schema 和模型如何定义的知识。然后,我们使用这些信息为 LocalLibrary 网站设计和实现了 Book
、BookInstance
、Author
和 Genre
模型。
最后,我们通过创建多个实例(使用独立脚本)测试了我们的模型。在下一篇文章中,我们将探讨创建一些页面来显示这些对象。
另见
- 数据库集成 (Express docs)
- Mongoose 网站 (Mongoose docs)
- Mongoose 指南 (Mongoose docs)
- 验证 (Mongoose docs)
- Schema 类型 (Mongoose docs)
- 模型 (Mongoose docs)
- 查询 (Mongoose docs)
- 填充 (Mongoose docs)