Bootstrap

【nest】nest结合typeorm基本使用

前言

  • 前面分别学习了nest与typeorm的基本使用,下面需要把2者结合起来。
  • 本篇任务:
    1、创建users、posts、role表,每个表字段不少于4个
    2、users和posts是一对多的关系(不要求一定创建外键)
    3、users和role是多对多的关系(不要求一定创建外键)
    4、users、posts、role的增删改操作
    5、查询用户列表,要同时查询出关联的posts和role的数据
    6、给用户分配角色的时候时候要加上事务
    7、上面的全部提供restfull api接口的方式
  • 官网资料:https://docs.nestjs.com/techniques/database

新建项目

  • 我们使用官网脚手架进行安装。
nest new yourproject
  • 同时需要安装typeorm与mysql,数据库搭建不在本篇内容。
npm install --save @nestjs/typeorm typeorm mysql

链接数据库

  • 对于数据库敏感信息,需要放置env里,nest提供了个包内部使用dotenv来解决:
$ npm i --save @nestjs/config
  • 关于配置env,中文文档和英文文档说法不太一样,中文文档那个应该过时了。
  • 如果没啥特别的配置,使用时在appmodule中import:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [ConfigModule.forRoot()],
})
export class AppModule {}
  • 使用自定义配置,需要建立配置文件导出 config/database.config.ts
export default () => ({
  type: process.env.DB_TYPE,
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT),
  database: process.env.DB_DATABASE,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  logging: true,
});
  • 然后appmodule下导入,同时配置typeorm:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import configuration from '../config/database.config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
    TypeOrmModule.forRoot(),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • 配置typeorm的ormconfig文件,用来连接数据库:
module.exports = [
  {
    name: 'default',
    type: process.env.DB_TYPE,
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    database: process.env.DB_DATABASE,
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    logging: false,
    synchronize: true,
    entities: ['dist/src/**/*.entity.{ts,js}'],
    migrations: ['src/migration/*.ts'],
    subscribers: ['src/subscriber/**/*.ts'],
    cli: {
      entitiesDir: 'src/',
      migrationsDir: 'src/migration',
      subscribersDir: 'src/subscriber',
    },
  },
];
  • 此时start,发现已经成功链接数据库了。
  • nest上要的env实际不是链接数据库,而是去提供了一个config的service,typeorm才是真正利用env链接得数据库。
  • 可以新建个模块来验证下:
  • 使用命令新建个模块三件套:
nest g mo user
nest g co user
nest g s user
  • module下导入它
@Module({
  imports: [ConfigModule],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}
  • 在控制器中,可以试着打印注入的config服务,如果能打印出来则成功:
@Controller('user')
export class UserController {
  constructor(private configService: ConfigService) {
    // get an environment variable
    const dbUser = this.configService.get<string>('DB_TYPE');
    // get a custom configuration value
    const dbHost = this.configService.get<string>('DB_PORT');
    console.log(dbUser, dbHost);
  }
}

编写实体

  • 首先创建user的实体,在其文件夹下建立user.entity.ts。
  • 因为ormconfig里配置了实体后缀,所以实体必须以此结尾,代码直接拷贝上次的
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  DeepPartial
} from 'typeorm';

@Entity({ name: 'user' })
export class UserEntity {
  @PrimaryGeneratedColumn({
    type: 'int',
    name: 'id',
    comment: '主键id',
  })
  id: number;

  @Column({
    type: 'varchar',
    nullable: false,
    length: 50,
    unique: true,
    name: 'username',
    comment: '用户名',
  })
  username: string;

  @Column({
    type: 'varchar',
    nullable: false,
    length: 100,
    comment: '密码',
  })
  password: string;

  @Column('tinyint', {
    nullable: false,
    default: () => 0,
    name: 'is_del',
    comment: '是否删除,1表示删除,0表示正常',
  })
  isDel: number;

  @CreateDateColumn({
    type: 'timestamp',
    nullable: false,
    name: 'created_at', 
    comment: '创建时间',
  })
  createdAt: Date;

  @UpdateDateColumn({
    type: 'timestamp',
    nullable: false,
    name: 'updated_at',
    comment: '更新时间',
  })
  updateAt: Date;
}
export type UserEntityDataType = UserEntityDataType = DeepPartial<UserEntity>;
  • module中需要导入实体:
@Module({
  imports: [ConfigModule, TypeOrmModule.forFeature([UserEntity ])],
  controllers: [UserController],
  providers: [UserService],
})
  • 先在服务中写个新增和查询:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { UserEntity ,UserEntityDataType} from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
  ) {}

  async createUser(data:UserEntityDataType): Promise<UserEntity> {
    return await this.userRepository.save(data);
  }

  async userList(): Promise<UserEntity[]> {
    return await this.userRepository.find();
  }
}
  • 在控制器中写入路由和服务:
import { Controller, Post, Body, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UserService } from './user.service';
import { UserEntity,UserEntityDataType } from './user.entity';
@Controller('user')
export class UserController {
  constructor(
    private configService: ConfigService,
    private readonly userService: UserService,
  ) {
    // get an environment variable
    const dbUser = this.configService.get<string>('DB_TYPE');
    // get a custom configuration value
    const dbHost = this.configService.get<string>('DB_PORT');
    console.log(dbUser, dbHost);
  }
  @Post()
  async createUser(
    @Body() data:UserEntityDataType,
  ): Promise<UserEntity> {
    return await this.userService.createUser(data);
  }
  @Get()
  async userList(): Promise<UserEntity[]> {
    return await this.userService.userList();
  }
}
  • 启动服务,拿postman之类玩意测试下。
  • 使用get访问http://localhost:3000/user 应该拿到空数组。
  • 然后使用Post新增个user:
{
	"username":"yehuozhili",
	"password":"12345"
}
  • 得到回复:
{
  "username": "yehuozhili",
  "password": "12345",
  "id": 1,
  "isDel": 0,
  "createdAt": "2020-09-08T22:24:09.569Z",
  "updateAt": "2020-09-08T22:24:09.569Z"
}
  • 查看数据库有写入就ok。
  • 下面照葫芦画瓢,把posts、role表创建好,users和posts是一对多,users和role是多对多的关系
  • 2个模块使用命令创建三件套,新建其实体。
  • posts实体抄上次的,role抄上次tag的。
  @ManyToOne(
    () => UserEntity,
    user => user.posts,
  )
  user: UserEntity;
  @ManyToMany(
    () => UserEntity,
    user => user.roles,
  )
  @JoinTable({ name: 'role_user' })
  users: UserEntity[];
  @OneToMany(
    () => Posts,
    post => post.user,
  )
  posts: Posts[];

  @ManyToMany(
    () => Roles,
    role => role.users,
  )
  roles: Roles[];
  • 实体制作完成,下面制作服务与控制器,别忘了引入实体到Module上。
  • 服务与控制器和user的写法基本一致,值得注意的就是外键保存的话需要注入额外的服务拿到Repository,我终于知道为啥service需要分离了,真是不写不知道。
  • 还需要写修改和删除,我就简单写写,无非就是传参问题。
  @Put()
  async changePassw(@Body() data: UserEntityDataType) {
    if (data.id !== undefined && data.password !== undefined) {
      return await this.userService.changePassword(data.id, data.password);
    } else {
      return 'you need to pass id and password';
    }
  }

  @Delete()
  async delUser(@Body() data: UserEntityDataType) {
    return this.userService.delUser(data.id);
  }
  • 服务就这样:
  async changePassword(id: number, newPassword: string): Promise<UserEntity> {
    const user = await this.userRepository.findOne(id);
    user.password = newPassword;
    return await this.userRepository.save(user);
  }

  async delUser(id: number): Promise<UserEntity> {
    const user = await this.userRepository.findOne(id);
    return await this.userRepository.remove(user);
  }
}
  • 这样就完成了
  • 开启服务试试效果。这里我已经测试过ok。

事务

  • 这里我们对创建role使用事物,创建一个role同时,分配给指定user 。
  • 关于多对多以及事务,这里有几个坑。
  • 首先多对多是有个中间表,也就是user或者role其中一个没加上对方,都会完全添加不上。
  • 第二个坑就是查询user时,user的表中role字段是不显示的,只有加入relation才显示,自己指定是无效的。
  • 第三个坑就是事务操作必须manager从头干到尾,不能使用repo进行保存,否则repo一旦save,必然存进数据库,而manager如果发现之中有错误,就算是save完再error,也能回滚。
  • 这里使用role的服务进行创建的transition操作:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Roles } from './roles.entity';
import { Repository, EntityManager } from 'typeorm';
import { UserEntity } from 'src/user/user.entity';
import { RoleCreateDataType } from './roles.controller';
import { isArray } from 'util';

@Injectable()
export class RolesService {
  constructor(
    @InjectRepository(Roles)
    private readonly roleRepository: Repository<Roles>,
  ) {}

  async create(
    data: RoleCreateDataType,
    manager: EntityManager,
  ): Promise<string> {
    const roles = new Roles();
    roles.name = data.role.name;
    const user = await manager
      .getRepository(UserEntity)
      .findOne(data.user.id, { relations: ['roles'] });
    console.log(user.roles);
    //找到的user里加入role
    if (isArray(user.roles)) {
      user.roles.push(roles);
    } else {
      user.roles = [roles];
    }

    roles.users = [user];
    console.log(user);
    await manager.save(user);

    await manager.save(roles);
    return 'ok';
  }

  async getList(): Promise<Roles[]> {
    return await this.roleRepository.find();
  }
}
  • 控制器使用transaction:
export type RoleCreateDataType = {
  user: UserEntityDataType;
  role: RolesTypes;
};

@Controller('roles')
export class RolesController {
  constructor(private readonly roleService: RolesService) {}
  @Post()
  @Transaction()
  async createUser(
    @Body()
    data: RoleCreateDataType,
    @TransactionManager() manager: EntityManager,
  ): Promise<string> {
    return await this.roleService.create(data, manager);
  }

  @Get()
  async userList(): Promise<Roles[]> {
    return await this.roleService.getList();
  }
}
;