by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
大家好,终于又和大家见面了。前面七章的项目,最终得到的前端雏形代码,集中放在front-base中了。
数据库介绍
前端的功能主要是页面效果的展示,后端的功能主要是对于各种数据的操作。NestJS可以操作数据库,就如同我们使用express和fastify来操作数据库一样。可以使用很多第三方的库,这里就不介绍了。
为了方便起见,NestJS集成了@nestjs/typeorm和nestjs/sequelize这2个库来操作数据库。如果使用mongo数据库的朋友可以参考官方文档,这里不讨论。
Sequelize 和 TypeORM 都是基于 JavaScript 的对象关系映射(ORM)库,用于在 Node.js 中操作关系型数据库。下面是它们的优缺点:
Sequelize 的优点:
- Sequelize 支持多种数据库,包括 PostgreSQL、MySQL、SQLite、MSSQL 等,可以很方便地进行数据库迁移。
- Sequelize 支持事务,可以很好地保证数据的一致性。
- Sequelize 的文档和社区支持都很好,可以轻松地找到解决问题的方法。
Sequelize 的缺点:
- Sequelize 在设计时对于 TypeScript 并没有做很好的支持,对于复杂的数据类型需要手动进行类型转换。
- Sequelize 对于查询语句的构建方式较为繁琐,代码可读性较差。
TypeORM 的优点:
- TypeORM 支持 TypeScript,提供了更好的类型检查和代码提示。
- TypeORM 可以使用 TypeScript 的装饰器语法定义实体,减少了重复代码。
- TypeORM 提供了更简单的查询语句构建方式,代码可读性更好。
TypeORM 的缺点:
- TypeORM 目前只支持几种数据库,如 PostgreSQL、MySQL、SQLite、MariaDB 等,不支持一些比较流行的数据库,如 Oracle、DB2 等。
- TypeORM 的文档和社区支持相对较弱,有些问题需要花费更多的时间来解决。
总体而言,Sequelize 和 TypeORM 都是很不错的 ORM 库,具体选择哪个库要看具体项目的需求和团队的技术栈。如果需要支持多种数据库,并且有复杂的事务操作,可以选择 Sequelize。如果项目中使用 TypeScript,并且需要更好的类型检查和代码提示,可以选择 TypeORM。
这个博客项目基本没有多个数据库和复杂的事务,所以暂时只介绍TypeORM的使用。
TypeORM 整合
TypeORM是一个比较成熟的对象关系映射(ORM)库。
在使用之前需要先导入必要的依赖包。这章我们会用MySql来做我们的数据库,所以也需要安装mysql的包。此外TypeORM还支持许多关系型或者非关系型数据库(请参照TypeORM官网)。
$ npm install --save @nestjs/typeorm typeorm mysql2
安装好依赖之后,我们就能在AppModule中导入TypeOrmModule模块
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
export class AppModule {}
警告:参数synchronize: true 不应该在正式环境中被设置-除非数据丢失也可以接受
forRoot()方法支持所有从TypeORM包中的DataSource构造器的参数。另外,还有几个附加参数如下:
| 名字 | 内容 |
|---|---|
retryAttempts | 尝试连接到数据库的次数 (default: 10) |
retryDelay | 连接重试尝试之间的延迟 (ms) (default: 3000) |
autoLoadEntities | 如果为“true”,将自动加载实体 (default: false) |
完成此操作后,TypeORM DataSource和EntityManager对象将可用于在整个项目中注入(无需导入任何模块),例如:
import { DataSource } from 'typeorm';
@Module({
imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
constructor(private dataSource: DataSource) {}
}
仓库模式
TypeORM支持存储库设计模式,现在来定义实体User
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
}
User实体文件位于users目录中。此目录包含与UsersModule相关的所有文件。您可以决定将模型文件保存在哪里,但是,我们建议在其域附近的相应模块目录中创建它们。
要开始使用User实体,我们需要通过将其插入模块forRoot()方法选项中的实体数组来让TypeORM了解它(除非您使用静态glob路径):
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [User],
synchronize: true,
}),
],
})
export class AppModule {}
接下来看UserModule
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
在这个模块中使用forFeature()方法来定义和注册存储库并在当前作用域中有效。我们可以在UserServcice中使用@injectReposeitory()装饰器来注入UsersRepository。
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}
async remove(id: number): Promise<void> {
await this.usersRepository.delete(id);
}
}
如果您想要在这个模块以外的模块中也使用它,必须要在此导出这个provider.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
exports: [TypeOrmModule]
})
export class UsersModule {}
假设我们在UserHttpModule中导入UsersModule,我们能够使用@InjectRepository(User)
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [UsersModule],
providers: [UsersService],
controllers: [UsersController]
})
export class UserHttpModule {}
关系
关系型数据库,表之间的关系需要通过主键或者外键来链接。关系分为以下几种。
| 关系名字 | 内容 |
|---|---|
One-to-one : 一对一 | 主表中的每一行在外部表中都有一个且只有一个关联行。使用@OneToOne()装饰器来定义这种类型的关系。 |
One-to-many / Many-to-one: 一对多/多对一 | 主表中的每一行在外部表中都有一个或多个相关行。使用@OneToMany()和@ManyToOne()装饰符来定义这种类型的关系。 |
Many-to-many : 多对多 | 主表中的每一行在外表中都有许多相关的行,外表中的每条记录在主表中也有许多相关行。使用@ManyToMany()装饰器来定义这种类型的关系。 |
在实体中定义关系,需要使用关系的装饰器,假设每个User都有多个照片,使用一对多@OneToMany()装饰器。
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
@OneToMany(type => Photo, photo => photo.user)
photos: Photo[];
}
自动加载实体
手动将实体添加到数据源选项的实体数组中可能很繁琐。此外,引用根模块中的实体会打破应用程序域边界,并导致实现细节泄露到应用程序的其他部分。为了解决这个问题,提供了一种替代解决方案。要自动加载实体,请将配置对象的autoLoadEntities属性(传递到forRoot()方法)设置为true,如下所示:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
...
autoLoadEntities: true,
}),
],
})
export class AppModule {}
指定该选项后,通过forFeature()方法注册的每个实体都将自动添加到配置对象的实体数组中。
分离实体定义
您可以使用decorator在模型中定义实体及其列。但有些人更喜欢使用“实体模式”在单独的文件中定义实体及其列。
import { EntitySchema } from 'typeorm';
import { User } from './user.entity';
export const UserSchema = new EntitySchema<User>({
name: 'User',
target: User,
columns: {
id: {
type: Number,
primary: true,
generated: true,
},
firstName: {
type: String,
},
lastName: {
type: String,
},
isActive: {
type: Boolean,
default: true,
},
},
relations: {
photos: {
type: 'one-to-many',
target: 'Photo', // the name of the PhotoSchema
},
},
});
# 警告:
如果提供`target`选项,则`name`选项值必须与目标类的名称相同。如果您没有提供`target`,您可以使用任何名称。
Nest允许我们使用EntitySchema实例
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserSchema } from './user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([UserSchema])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
TypeORM批处理
数据库事务象征着在数据库管理系统中针对数据库执行的工作单元,并以独立于其他事务的一致和可靠的方式进行处理。事务通常表示数据库中的任何更改
有许多不同的策略来处理TypeORM事务。建议使用QueryRunner类,因为它可以完全控制事务。
首先,我们需要以正常方式将DataSource对象注入到类中:
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
}
我们能够使用这个对象来创建事务
async createMany(users: User[]) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(users[0]);
await queryRunner.manager.save(users[1]);
await queryRunner.commitTransaction();
} catch (err) {
// 发生错误的情况下回滚事物
await queryRunner.rollbackTransaction();
} finally {
// 您需要释放一个手动实例化的queryRunner
await queryRunner.release();
}
}
或者,您可以将回调样式方法与DataSource对象的事务方法一起使用(阅读更多)。
async createMany(users: User[]) {
await this.dataSource.transaction(async manager => {
await manager.save(users[0]);
await manager.save(users[1]);
});
}
订阅者
通过TypeORM订阅者subscribers,可以监听一些特殊的事件。
import {
DataSource,
EntitySubscriberInterface,
EventSubscriber,
InsertEvent,
} from 'typeorm';
import { User } from './user.entity';
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
constructor(dataSource: DataSource) {
dataSource.subscribers.push(this);
}
listenTo() {
return User;
}
beforeInsert(event: InsertEvent<User>) {
console.log(`BEFORE USER INSERTED: `, event.entity);
}
}
# 警告
不能将事件订阅服务器设置为请求作用域(https://docs.nestjs.com/fundamentals/injection-scopes)。
然后把UserSubscriber加入providers数组中
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserSubscriber } from './user.subscriber';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService, UserSubscriber],
controllers: [UsersController],
})
export class UsersModule {}
多个数据库
有些项目需要多个数据库连接。这也可以通过该模块来实现。若要使用多个连接,请首先创建连接。在这种情况下,数据源命名成为强制性的。
假设您有一个album实体存储在它自己的数据库中。
const defaultOptions = {
type: 'postgres',
port: 5432,
username: 'user',
password: 'password',
database: 'db',
synchronize: true,
};
@Module({
imports: [
TypeOrmModule.forRoot({
...defaultOptions,
host: 'user_db_host',
entities: [User],
}),
TypeOrmModule.forRoot({
...defaultOptions,
name: 'albumsConnection',
host: 'album_db_host',
entities: [Album],
}),
],
})
export class AppModule {}
如果不设置数据源的名称name,则其名称将设置为默认值。请注意,您不应该有多个没有名称或具有相同名称的连接,否则它们将被覆盖。
如果您使用TypeOrmModule.forRootAsync,您还必须在useFactory之外设置数据源名称。例如:
TypeOrmModule.forRootAsync({
name: 'albumsConnection',
useFactory: ...,
inject: ...,
}),
然后,可以通过下面的方法来导入。
@Module({
imports: [
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([Album], 'albumsConnection'),
],
})
export class AppModule {}
您还可以为给定的数据源注入DataSource或EntityManager:
@Injectable()
export class AlbumsService {
constructor(
@InjectDataSource('albumsConnection')
private dataSource: DataSource,
@InjectEntityManager('albumsConnection')
private entityManager: EntityManager,
) {}
}
还可以向提供程序注入任何DataSource:
@Module({
providers: [
{
provide: AlbumsService,
useFactory: (albumsConnection: DataSource) => {
return new AlbumsService(albumsConnection);
},
inject: [getDataSourceToken('albumsConnection')],
},
],
})
export class AlbumsModule {}
测试
当涉及到应用程序的单元测试时,我们通常希望避免建立数据库连接,保持测试套件的独立性及其执行过程尽可能快。但我们的类可能依赖于从数据源(连接)实例中提取的存储库。我们该如何处理?解决方案是创建模拟存储库。为了实现这一点,我们建立了自定义provider。每个注册的存储库都由<EntityName>repository令牌自动表示,其中EntityName是实体类的名称。
@nestjs/typeorm包公开了getRepositoryToken()函数,该函数返回基于给定实体准备的令牌。
@Module({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
})
export class UsersModule {}
现在,一个替代的mockRepository将被用作UsersRepository。每当任何类使用@InjectRepository()装饰器请求UsersRepository时,Nest都会使用注册的mockRepository对象。
异步配置
您可能希望异步传递存储库模块选项,而不是静态传递。在这种情况下,使用forRootAsync()方法,该方法提供了几种处理异步配置的方法。
一种方法是使用工厂函数:
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
});
我们的工厂的行为与任何其他异步porvider程序类似(例如,它可以是异步的,并且能够通过注入注入依赖项)。
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('HOST'),
port: +configService.get('PORT'),
username: configService.get('USERNAME'),
password: configService.get('PASSWORD'),
database: configService.get('DATABASE'),
entities: [],
synchronize: true,
}),
inject: [ConfigService],
});
或者,您可以使用useClass语法:
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
});
上面的构造将在TypeOrmModule内实例化TypeOrmConfigService,并使用它通过调用createTypeOrmOptions()来提供一个选项对象。请注意,这意味着TypeOrmConfigService必须实现TypeOrmOptionsFactory接口,如下所示:
@Injectable()
class TypeOrmConfigService implements TypeOrmOptionsFactory {
createTypeOrmOptions(): TypeOrmModuleOptions {
return {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
};
}
}
为了防止在TypeOrmModule内创建TypeOrmConfigService并使用从其他模块导入的提供程序,可以使用useExisting语法。
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
这种构造与useClass的工作原理相同,但有一个关键区别——TypeOrmModule将查找导入的模块来重用现有的ConfigService,而不是实例化新的ConfigService。
创建后端项目并导入TypeORM
官方的例子讲的差不多了,我们来试着自己导入MySql数据库。首先和前端项目一样,先把一些基础的模块先做出来。
- fasify
- Config配置
- 日志系统
- 异常过滤,拦截器
这当中有些需要修改的地方,比如配置文件,增加了数据相关的配置
SERVER_VALUE:
host: '0.0.0.0'
port: 8001
LOG_CONFIG:
TIMESTAMP: true
LOG_LEVEL: 'info'
LOG_ON: true
MYSQL_CONFIG:
synchronize: true
database: 'juejin-blogdb'
host: '127.0.0.1'
port: '3306'
username: 'root'
password: '123456'
type: 'mysql'
配置增加了,配置文件验证规则也要增加
/**
* 对象验证逻辑
*/
const shcema = Joi.object().keys({
SERVER_VALUE: Joi.object({
port: Joi.number().default(8000),
host: Joi.string().required(),
}),
LOG_CONFIG: Joi.object({
TIMESTAMP: Joi.boolean().default(false),
LOG_LEVEL: Joi.string().valid('info', 'error').required(),
LOG_ON: Joi.boolean().default(false),
}),
// 追加
MYSQL_CONFIG: Joi.object({
synchronize: Joi.boolean().default(true),
database: Joi.string().required(),
host: Joi.string().default('127.0.0.1'),
port: Joi.string().default('3306'),
username: Joi.string().default('root'),
password: Joi.string().default('123456'),
type: Joi.string().default('mysql'),
})
});
1. 安装数据库依赖
pnpm i @nestjs/typeorm typeorm mysql2
2 数据库的导入配置以及实体文件
common/database/ormconfig.ts中写如下代码,根据传过来的configService来创建mysql数据库连接。
import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigEnum } from '../enum/config.enum';
export function buildConnectionOptions(configService: ConfigService) {
const config = configService.get(ConfigEnum.MYSQL_CONFIG);
const logFlag = configService.get(ConfigEnum.LOG_CONFIG)['LOG_ON'] === 'true';
// 扫描文件夹下所有 mysql.entity.ts 后缀结尾的文件
const entitiesDir =
process.env.NODE_ENV === 'test'
? [__dirname + `/**/*.${config.type}.entity.ts`]
: [__dirname + `/**/*.${config.type}.entity{.js,.ts}`];
return {
...config,
entities: entitiesDir,
logging: logFlag,
} as TypeOrmModuleOptions;
}
实体文件common/database/entities/user.mysql.entity.ts,定义表的实体结构,实体放在这个位置是我个人的喜好,按照官方的习惯,应该把实体放到模块里面。
import {
Column,
Entity,
PrimaryColumn,
} from 'typeorm';
@Entity()
export class User {
@PrimaryColumn({
length: 30,
})
userid: string;
@Column({
length: 30,
default: null,
})
username: string;
@Column({
length: 64,
default: null,
})
password: string;
@Column({
type: 'tinyint',
default: 1,
})
status: number;
}
3 app.module.ts中配置数据库
import { TypeOrmModule } from '@nestjs/typeorm';
import { buildConnectionOptions } from './common/database/ormconfig';
import { UserModule } from './modules/user/user.module';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: buildConnectionOptions,
inject: [ConfigService],
}),
],
})
export class AppModule {}
4. 创建user模块
先运行下面的模块
nest g resource modules/user --no-spec
# 选择 REST API
# 选择创建CRUD
删除不需要额代码和文件,最后精简成下面这样
user.module.ts中导入typeORM
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/common/database/entities/user.mysql.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UserController],
providers: [UserService]
})
export class UserModule {}
user.controller.ts保留 一个创建的方法
import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
}
user.service.ts 写数据库相关的操作
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from 'src/common/database/entities/user.mysql.entity';
import { BusinessException } from 'src/common/exceptions/business.exception';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly articleRepository: Repository<User>,
) {}
/**
* 创建用户
* @param createUserDto
* @returns
*/
async create(createUserDto: CreateUserDto) {
let res;
const tempArticle = await this.articleRepository.create(createUserDto);
res = await this.articleRepository.save(tempArticle);
if (res) {
return 'success';
} else {
throw new BusinessException('用户创建失败');
}
}
}
create-user.dto.ts文件内容如下
export class CreateUserDto {
userid: string;
password: string;
username: string;
status: number;
}
最后用postman测试下,看看是否成功
确认数据库里面的内容是否正确被插入
如果大家觉得这篇文章对您有帮助,别忘了点赞/评论。谢谢🙏了。