数据库¶
Nest 与数据库无关,允许您轻松地与任何 SQL 或 NoSQL 数据库集成。 根据您的偏好,您有许多可供选择的选项。 在最一般的层面上,连接 Nest 到数据库只是一个简单的问题,为数据库加载一个合适的 Node.js 驱动程序,就像你使用Express或 fasttify 一样。
你也可以直接使用任何通用的 Node.js 数据库集成 库 或 ORM, such as MikroORM also check the recipe here, Sequelize (navigate to the Sequelize integration section), Knex.js (tutorial), TypeORM, and Prisma (recipe) , 以在更高的抽象级别上操作。
为了方便起见,Nest 提供了与 TypeORM 和 Sequelize 的紧密集成,它们分别是@nestjs/typeorm
和@nestjs/sequelize
包,我们将在本章中介绍,而 Mongoose 与@nestjs/mongoose
包,这在本章中介绍。
这些集成提供了额外的特定于 nestjs
的特性,例如模型/存储库注入、可测试性和异步配置,使访问您选择的数据库更加容易。
TypeORM 集成¶
为了集成 SQL 和 NoSQL 数据库,Nest 提供了@nestjs/typeform
包。
Nest 使用TypeORM是因为它是 TypeScript 可用的最成熟的对象关系映射器(Object Relational Mapper, ORM)。
因为它是用 TypeScript 编写的,所以可以很好地与 Nest 框架集成。
要开始使用它,我们首先安装所需的依赖项。 在本章中,我们将演示使用流行的MySQL关系数据库管理系统,但 TypeORM 提供了许多关系数据库的支持,如 PostgreSQL, Oracle, Microsoft SQL Server, SQLite,甚至 NoSQL 数据库,如 MongoDB。 对于 TypeORM 支持的任何数据库,我们在本章中所经历的过程都是相同的。 您只需要为所选数据库安装相关的客户端 API 库。
一旦安装过程完成,我们就可以把TypeOrmModule
导入到根目录AppModule
中。
Warning
设置synchronize: true
不应该在生产中使用,否则您可能会丢失生产数据。
forRoot()
方法支持所有由TypeORM包中的createConnection()
函数公开的配置属性。
此外,下面还描述了几个额外的配置属性。
retryAttempts |
尝试连接数据库的次数 (default: 10 ) |
retryDelay |
连接重试之间的延迟(ms) (default: 3000 ) |
autoLoadEntities |
如果true ,实体将被自动加载 (default: false ) |
keepConnectionAlive |
如果true ,连接将不会在应用程序关闭时关闭 (default: false ) |
Hint
有关连接选项的更多信息这里。
或者,不需要将配置对象传递给forRoot()
,我们可以在项目根目录创建一个ormconfig.json
文件。
然后,我们可以不带任何选项调用forRoot()
:
Warning
静态的 glob 路径(e.g., dist/** /*.entity{ .ts,.js}
)不会正常工作webpack.
Hint
注意ormconfig.json
文件是由typeform
库加载的。
因此,上面描述的任何额外属性(通过内部的
forRoot()
方法支持-例如,autoLoadEntities
和retryDelay
)将不会被应用。 幸运的是,TypeORM 提供了getConnectionOptions
函数,该函数从ormconfig
文件或环境变量中读取连接选项。 这样,你仍然可以使用配置文件并设置特定于 nest 的选项,如下所示:
一旦完成,TypeORM Connection
和EntityManager
对象将可以在整个项目中注入(不需要导入任何模块),例如:
库模式¶
TypeORM支持 库设计模式 ,因此每个实体都有自己的库。 这些存储库可以从数据库连接中获得。
为了继续这个例子,我们至少需要一个实体。 让我们定义“用户”实体。
Hint
关于实体的更多信息,请参阅TypeORM 文档。
User
实体文件位于users
目录中。
这个目录包含所有与UsersModule
相关的文件。
你可以决定在哪里保存你的模型文件,然而,我们建议在它们的 域 附近创建它们,在相应的模块目录中。
为了开始使用User
实体,我们需要将它插入到模块的forRoot()
方法选项中的entities
数组中,让 TypeORM 知道它(除非你使用一个静态的 glob 路径):
接下来,让我们看看UsersModule
:
这个模块使用forFeature()
方法来定义哪些存储库注册在当前范围内。
有了它,我们就可以使用@InjectRepository()
装饰器将UsersRepository
注入到UsersService
中:
Warning
别忘了把UsersModule
导入根模块AppModule
。
如果你想使用模块外部的存储库,该模块导入了TypeOrmModule.forFeature()
,你需要重新导出它生成的提供器。
你可以通过导出整个模块来实现,像这样:
现在,如果我们在UserHttpModule
中导入UsersModule
,我们可以在后一个模块的 providers 中使用@InjectRepository(User)
。
关系¶
关系是在两个或多个表之间建立的关联。 关系基于每个表的公共字段,通常涉及主键和外键。
有三种关系:
One-to-one |
主表中的每一行在外部表中有且只有一个关联行。使用@OneToOne() 装饰器来定义这种类型的关系。 |
One-to-many / Many-to-one |
主表中的每一行在外部表中都有一个或多个相关行。使用@OneToMany() 和@ManyToOne() 装饰器来定义这种类型的关系。 |
Many-to-many |
主表中的每一行在外部表中有许多相关行,而外部表中的每条记录在主表中有许多相关行。使用@ManyToMany() 装饰器来定义这种类型的关系。 |
要定义实体中的关系,请使用相应的 装饰器 。
例如,要定义每个User
可以有多个照片,请使用@OneToMany()
装饰器。
Hint
要了解 TypeORM 中的更多关系,请访问TypeORM 文档。
自动加载实体¶
手动添加实体到连接选项的entities
数组中可能很繁琐。
此外,从根模块引用实体会打破应用程序域边界,并导致实现细节泄露到应用程序的其他部分。
为了解决这个问题,可以使用静态 glob 路径 (e.g., dist/**/*.entity{ .ts,.js}
).
但是请注意,webpack 不支持 glob 路径,所以如果你在 monorepo 中构建你的应用程序,你将无法使用它们。
为了解决这个问题,我们提供了另一种解决方案。
要自动加载实体,需要将配置对象(传入forRoot()
方法)的autoLoadEntities
属性设置为true
,如下所示:
指定该选项后,每个通过forFeature()
方法注册的实体都会自动添加到配置对象的entities
数组中。
Warning
请注意,没有通过forFeature()
方法注册的实体,但仅从实体引用(通过关系),将不包括在autoLoadEntities
设置的方式。
分离的实体定义¶
您可以使用装饰器在模型中直接定义实体及其列。
但是有些人喜欢使用实体模式
在单独的文件中定义实体和它们的列。
warning error Warning 如果您提供了
target
选项,name
选项的值必须与目标类的名称相同。 如果您不提供target
,您可以使用任何名称。
嵌套允许你在任何需要Entity
的地方使用EntitySchema
实例,例如:
事务¶
数据库事务代表在数据库管理系统中针对数据库执行的工作单元,并以独立于其他事务的一致和可靠的方式处理。 事务通常代表数据库中的任何变化(了解更多信息)。
有许多不同的策略来处理TypeORM 事务。
我们建议使用QueryRunner
类,因为它提供了对事务的完全控制。
首先,我们需要以正常的方式将Connection
对象注入到类中:
Hint
Connection 类是从 type 包中导入的。
现在,我们可以使用这个对象来创建事务。
Hint
注意connection
仅用于创建QueryRunner
。
但是,要测试这个类,需要模拟整个
Connection
对象(它公开了几个方法)。 因此,我们建议使用一个助手工厂类(例如,QueryRunnerFactory
),并定义一个接口,该接口包含一组维护事务所需的有限方法。 这种技术使得模仿这些方法非常简单。
或者,你可以使用回调风格的方法,使用Connection
对象的transaction
方法(read more)。
不建议使用装饰器来控制事务(@Transaction()
和@TransactionManager()
)。
订阅者¶
使用 TypeORM订阅者,您可以监听特定的实体事件。
error Warning 事件订阅者不能以请求为范围.
现在,将UserSubscriber
类添加到providers
数组中:
Hint
了解更多实体订阅者此处.
迁移¶
Migrations提供了一种方法来增量地更新数据库模式,使其与应用程序的数据模型保持同步,同时保留数据库中的现有数据。 为了生成、运行和恢复迁移,TypeORM 提供了一个专用的CLI.
迁移类与 Nest 应用程序源代码是分开的。 它们的生命周期是由 TypeORM CLI 维护的。 因此,您不能通过迁移来利用依赖注入和其他 Nest 特定的特性。 要了解更多关于迁移的信息,请参考TypeORM 文档中的指南。
多个数据库¶
有些项目需要多个数据库连接。 这也可以通过这个模块实现。 要处理多个连接,首先要创建连接。 在这种情况下,连接命名成为 必须的 。
假设您有一个Album
实体存储在自己的数据库中。
Warning
如果你没有为一个连接设置name
,它的名称将被设置为default
。
请注意,您不应该有多个没有名称或具有相同名称的连接,否则它们将被覆盖。
此时,你有User
和Album
实体注册到它们自己的连接。
在这个设置中,你必须告诉TypeOrmModule.forFeature()
方法和@InjectRepository()
装饰器应该使用哪个连接。
如果您没有传递任何连接名称,则使用default
连接。
你也可以为一个给定的连接注入Connection
或EntityManager
:
也可以将任何Connection
注入到提供器:
测试¶
当涉及到应用程序的单元测试时,我们通常希望避免建立数据库连接,保持测试套件的独立性,并尽可能快地执行它们。
但是我们的类可能依赖于从连接实例中提取的存储库。
我们该如何处理呢?解决方案是创建模拟存储库。
为了实现这个目标,我们设置了custom providers。
每个注册的存储库都自动由<EntityName>Repository
令牌表示,其中EntityName
是您的实体类的名称。
@nestjs/typeform
包公开了getRepositoryToken()
函数,该函数根据给定的实体返回一个准备好的令牌。
现在,将使用一个替代的mockRepository
作为UsersRepository
。
每当任何类使用@InjectRepository()
装饰器请求UsersRepository
时,Nest 就会使用注册的mockRepository
对象。
自定义库¶
TypeORM 提供了一个叫做自定义库的特性。 自定义存储库允许您扩展基存储库类,并使用几个特殊的方法充实它。 要了解更多关于此功能的信息,请访问本页。
为了创建您的自定义存储库,请使用@EntityRepository()
装饰器并扩展repository
类。
Hint
@EntityRepository()
和Repository
都是从typeform
包中导入的。
一旦创建了类,下一步就是将实例化责任委托给 Nest。
为此,我们必须将AuthorRepository
类传递给TypeOrm.forFeature()
方法。
之后,只需使用下面的构造注入存储库:
异步的配置¶
您可能希望异步传递存储库模块选项,而不是静态传递。
在这种情况下,使用forRootAsync()
方法,它提供了几种处理异步配置的方法。
一种方法是使用工厂函数:
我们的工厂的行为和其他异步提供器一样(例如,它可以是async
,并且可以通过inject
注入依赖项)。
或者,你可以使用useClass
语法:
上面的构造将在 TypeOrmModule
中实例化 TypeOrmConfigService
,并通过调用 createTypeOrmOptions()
来使用它来提供一个选项对象。
注意,这意味着 TypeOrmConfigService
必须实现 TypeOrmOptionsFactory
接口,如下所示:
为了防止在TypeOrmModule
中创建 TypeOrmConfigService
,并使用从不同模块导入的提供器,你可以使用 useExisting
语法。
这个构造的工作原理与useClass
相同,但有一个关键的区别——TypeOrmModule
将查找导入的模块来重用现有的ConfigService
,而不是实例化一个新的。
Hint
确保name
属性与useFactory
、useClass
或useValue
属性定义在同一级别。
这将允许 Nest 在适当的注入令牌下正确地注册连接。
自定义连接工厂¶
在使用useFactory
, useClass
,或useExisting
的 async 配置中,你可以选择指定一个connectionFactory
函数,它将允许你提供自己的 TypeORM 连接,而不是允许TypeOrmModule
来创建连接。
connectionFactory
接收到在异步配置期间使用useFactory
, useClass
或useExisting
配置的 TypeORM ConnectionOptions
,并返回一个Promise
来解析 TypeORM Connection
。
Hint
createConnection
函数是从typeform
包中导入的。
例子¶
一个可用的例子在这里.
Sequelize 集成¶
使用 TypeORM 的另一个选择是使用Sequelize ORM 和@nestjs/sequelize
包。
此外,我们还利用了sequelize-typescript包,它提供了一组额外的装饰器来声明性地定义实体。
要开始使用它,我们首先安装所需的依赖项。 在本章中,我们将演示如何使用流行的MySQL关系数据库管理系统,但是 Sequelize 提供了对许多关系数据库的支持,如 PostgreSQL、MySQL、Microsoft SQL Server、SQLite 和 MariaDB。 对于 Sequelize 支持的任何数据库,我们在本章中所经历的过程都是相同的。 您只需要为所选数据库安装相关的客户端 API 库。
一旦安装完成,我们就可以把SequelizeModule
导入到根目录AppModule
中。
forRoot()方法支持 Sequelize 构造函数公开的所有配置属性(read more)。 此外,下面还描述了几个额外的配置属性。
retryAttempts |
尝试连接数据库的次数 (default: 10 ) |
retryDelay |
连接重试之间的延迟(ms) (default: 3000 ) |
autoLoadModels |
如果true ,模型将自动加载 (default: false ) |
keepConnectionAlive |
如果true ,连接将不会在应用程序关闭时关闭 (default: false ) |
synchronize |
如果true ,自动加载的模型将被同步 (default: true ) |
一旦完成,Sequelize
对象将可以在整个项目中注入(不需要导入任何模块),例如:
模型¶
Sequelize
实现活动记录模式。
使用这个模式,您可以直接使用模型类与数据库交互。
为了继续这个例子,我们至少需要一个模型。
让我们定义User
模型。
Hint
了解更多可用的 decorator这里.
User
模型文件位于users
目录中。
这个目录包含所有与UsersModule
相关的文件。
你可以决定在哪里保存你的模型文件,然而,我们建议在它们的 域 附近创建它们,在相应的模块目录中。
为了开始使用“User”模型,我们需要把它插入到模块的“forRoot()”方法选项中的“models”数组中,让 Sequelize 知道它:
接下来,让我们看看“UsersModule”:
这个模块使用forFeature()
方法来定义哪些模型注册在当前范围内。
有了它,我们就可以使用@InjectModel()
装饰器将UserModel
注入到UsersService
中:
Warning
别忘了把UsersModule
导入根模块AppModule
。
如果你想在导入SequelizeModulefor.Feature
的模块外部使用存储库,你需要重新导出它生成的提供器。
你可以通过导出整个模块来实现,像这样:
现在,如果我们在UserHttpModule
中导入UsersModule
,我们可以在后一个模块的providers
中使用@InjectModel(User)
。
关系¶
关系是在两个或多个表之间建立的关联。 关系基于每个表的公共字段,通常涉及主键和外键。
有三种关系:
One-to-one |
主表中的每一行在外部表中有且只有一个关联行 |
One-to-many / Many-to-one |
主表中的每一行在外部表中都有一个或多个相关行 |
Many-to-many |
主表中的每一行在外部表中有许多相关行,而外部表中的每条记录在主表中有许多相关行 |
要定义实体中的关系,请使用相应的 装饰器 。
例如,要定义每个User
可以有多个照片,请使用@HasMany()
装饰器。
Hint
要在 Sequelize 中了解更多有关关联的信息,请阅读this这一章。
自动负载模型¶
手动添加模型到连接选项的models
数组中可能会很繁琐。
此外,从根模块引用模型会打破应用程序域边界,并导致实现细节泄露到应用程序的其他部分。
为了解决这个问题,通过将配置对象的autoLoadModels
和synchronize
属性(传入到forRoot()
方法中)设置为true
来自动加载模型,如下所示:
指定了这个选项后,每个通过forFeature()
方法注册的模型都会自动添加到配置对象的models
数组中。
Warning
请注意,没有通过forFeature()
方法注册的模型,但仅从模型引用(通过关联),将不包括在内。
事务¶
数据库事务代表在数据库管理系统中针对数据库执行的工作单元,并以独立于其他事务的一致和可靠的方式处理。 事务通常代表数据库中的任何变化(了解更多信息)。
有许多不同的策略来处理Sequelize transaction。 下面是托管事务(自动回调)的示例实现。
首先,我们需要以正常的方式将Sequelize
对象注入到类中:
Hint
Sequelize
类是从 sequelize-typescript
包中导入的。
现在,我们可以使用这个对象来创建事务。
Hint
Note that the Sequelize
instance is used only to start the transaction.
However, to test this class would require mocking the entire
Sequelize
object (which exposes several methods). Thus, we recommend using a helper factory class (e.g.,TransactionRunner
) and defining an interface with a limited set of methods required to maintain transactions. This technique makes mocking these methods pretty straightforward.
迁移¶
Migrations提供了一种方法来增量地更新数据库模式,使其与应用程序的数据模型保持同步,同时保留数据库中的现有数据。 为了生成、运行和恢复迁移,Sequelize 提供了一个专用的CLI。
迁移类与 Nest 应用程序源代码是分开的。 它们的生命周期是由 Sequelize CLI 维护的。 因此,您不能通过迁移来利用依赖注入和其他 Nest 特定的特性。 要了解关于迁移的更多信息,请参考Sequelize 文档中的指南。
多个数据库¶
有些项目需要多个数据库连接。 这也可以通过这个模块实现。 要处理多个连接,首先要创建连接。 在这种情况下,连接命名成为 必须的 。
假设您有一个Album
实体存储在自己的数据库中。
Warning
如果你没有为一个连接设置“name”,它的名称将被设置为“default”。
请注意,您不应该有多个没有名称或具有相同名称的连接,否则它们将被覆盖。
在这一点上,你有User
and Album
模型注册到他们自己的连接。
在这个设置中,你必须告诉SequelizeModule.forFeature()
方法和@InjectModel()
装饰器应该使用哪个连接。
如果您没有传递任何连接名称,则使用default
连接。
你也可以为一个给定的连接注入Sequelize
实例:
也可以将任何Sequelize
实例注入到提供器:
测试¶
当涉及到应用程序的单元测试时,我们通常希望避免建立数据库连接,保持测试套件的独立性,并尽可能快地执行它们。
但是我们的类可能依赖于从连接实例中提取的模型。
我们该如何处理呢?解决方案是创建模拟模型。
为了实现这个目标,我们设置了custom providers。
每个注册的模型都由一个<ModelName>model
令牌自动表示,其中ModelName
是您的模型类的名称。
@nestjs/sequelize
包公开了getModelToken()
函数,该函数根据给定的模型返回一个准备好的令牌。
现在,mockModel
将被用作UserModel
。
当任何类使用@InjectModel()
装饰器请求UserModel
时,Nest 将使用注册的mockModel
对象。
异步的配置¶
你可能想要异步传递你的“SequelizeModule”选项,而不是静态的。
在这种情况下,使用forRootAsync()
方法,它提供了几种处理异步配置的方法。
一种方法是使用工厂函数:
我们的工厂的行为和其他异步提供器一样(例如,它可以是async
,并且可以通过inject
注入依赖项)。
或者,你可以使用useClass
语法:
上面的构造将在SequelizeModule
实例化SequelizeConfigService
,并通过调用createSequelizeOptions()
来使用它来提供一个选项对象。
注意,这意味着SequelizeConfigService
必须实现SequelizeOptionsFactory
接口,如下所示:
为了防止在SequelizeModule
中创建SequelizeConfigService
,并使用从不同模块导入的提供器,你可以使用useExisting
语法。
这个构造的工作原理与useClass
相同,但有一个关键的区别——SequelizeModule
将查找导入的模块来重用现有的ConfigService
,而不是实例化一个新的。
例子¶
一个可用的例子在这里.