NestJS博客实战08-创建后端项目以及TypeORM介绍

1,390 阅读4分钟
by 雪隐 from https://juejin.cn/user/1433418895994094
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权

大家好,终于又和大家见面了。前面七章的项目,最终得到的前端雏形代码,集中放在front-base中了。

数据库介绍

前端的功能主要是页面效果的展示,后端的功能主要是对于各种数据的操作。NestJS可以操作数据库,就如同我们使用expressfastify来操作数据库一样。可以使用很多第三方的库,这里就不介绍了。

为了方便起见,NestJS集成了@nestjs/typeormnestjs/sequelize这2个库来操作数据库。如果使用mongo数据库的朋友可以参考官方文档,这里不讨论。

SequelizeTypeORM 都是基于 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 DataSourceEntityManager对象将可用于在整个项目中注入(无需导入任何模块),例如:

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 {}

您还可以为给定的数据源注入DataSourceEntityManager

@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数据库。首先和前端项目一样,先把一些基础的模块先做出来。

  1. fasify
  2. Config配置
  3. 日志系统
  4. 异常过滤,拦截器

这当中有些需要修改的地方,比如配置文件,增加了数据相关的配置

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测试下,看看是否成功

createuser.jpg

确认数据库里面的内容是否正确被插入

data.jpg

如果大家觉得这篇文章对您有帮助,别忘了点赞/评论。谢谢🙏了。

本章代码

代码