nest中使用prisma

225 阅读4分钟

前语:上篇nest 中也讲到了一些简单的步骤和操作,这篇主要是想深入将prisma的一些操作

1.orm

orm (object relational mappping) 对象关系映射 就是操作对象的方式操作数据库。

常见的ORM有prisma TypeORM Mongooseq 前两者支持多种数据库 后者为MongoDB的ORM.

prisma优点直观的数据模型、自动化迁移、类型安全、自动补全

数据模型,prisma把数据库中的表映射成model。 进行Migrate迁移prisma,就会根据你定义的数据模型,修改数据库。

image.png

运行npx prisma studio,会打开一个网页,展示数据库

image.png

2.prisma使用

//安装prisma
pnpm install prisma --save-dev
或者 npx prisma

//创建Prisma Schema
npx prisma init

会有schema.prisma文件和.env 文件 在schema.prisma可以定义自己数据模型 .env中有要连接的数据信息,可以用docker 跑一个数据库,然后连接

npx prisma migrate dev --name init

会有针对数据库运行SQL迁移文件

image.png

之后安装@prisma/client

pnpm install @prisma/client

连接我们的数据库也会有这些prisma模型生成的表

image.png

完成这些步骤之后就可以进行数据库查询了

初始化的时候 想插入一些初始数据 可以执行seed操作

npx prisma db seed

首先在prisma文件夹中创建文件 写一些初始化的数据

import { hash } from '@node-rs/bcrypt';
import { Prisma, PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const ADMIN_USERNAME = 'admin';
const ADMIN_NICKNAME = 'Ivy-Rong';
const ADMIN_PASSWORD = '123456';

async function main() {
  const adminUserInfo = Prisma.validator<Prisma.UserCreateInput>()({
    username: ADMIN_USERNAME,
    nickName: ADMIN_NICKNAME,
    password: await hash(ADMIN_PASSWORD),
    enabled: true,
  });

  const adminUser = await prisma.user.findUnique({
    where: {
      username: ADMIN_USERNAME,
    },
  });

  if (!adminUser) {
    await prisma.user.create({
      data: {
        ...adminUserInfo,
      },
    });
  }
}
main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

可以在package.json 写入脚本

"prisma": {  
    "seed": "ts-node prisma/seed/seed.ts"  
},

最后执行命令npx prisma db seed

image.png

可以看到数据库中有数据了,并且将密码使用hash进行加密了

image.png

3.Prisma 深入了解

1.Prisma schema

enum AuthTypeEnum {
  USER
  COOGLE
  GITHUB
}


model User {
  id           Int        @id @default(autoincrement())
  authType    AuthTypeEnum @default(USER) @map("auth_type")
  username     String     @unique @db.VarChar(30)
  nickName     String?    @map("nick_name") @db.VarChar(50)
  password     String     @db.VarChar(100)
  avatarUrl    String?    @map("avatar_url") @db.VarChar(100)
  gender       String?    @db.VarChar(50)
  enabled      Boolean    @default(true)
  accessToken  String?    @map("access_token")
  refreshToken String?    @map("refresh_token")
  UserRole     UserRole[]
  
  createdAt DateTime  @default(now()) @map("created_at") @db.Timestamp(3)
  createdBy Int?      @map("created_by")
  updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(3)
  updatedBy Int?      @map("updated_by")
  deletedAt DateTime? @map("deleted_at") @db.Timestamp(3)
  deletedBy Int?      @map("deleted_by")


  @@map("system_user")
}

  • 影响字段的属性用 @ 作为前缀,影响块的属性用 @@作为前缀 @@map("system_user")映射数据库表user
  • @id 唯一字段 (设置主键 PRIMARY KEY
  • @default 默认值
  • @unique唯一约束 字段唯一
  • @relation设置外键 用于建立表与表之间的关联
  • @map映射数据库中的字段
  • @updateAt 自动存储记录更新的时间

修改完更新数据库 可以看到增加了新的字段

image.png

image.png

2.关系

一个用户可以有多种鉴权方式,就是一个User 和 Auth 之间一对多 则可以建立两张表之间的联系

model User { 
    id  Int @id @default(uuid()) 
    auth Auth[] 
    }
model Auth { 
    id   Int   @id @default(cuid()) 
    user  User    @relation(fields: [userId], references: [id])
    userId Int   @map("user_id")
}

其中 @relation(fields: [userId], references: [id])表示 Auth 的 userId 字段与 User 的 id 字段建立关系,这两个字段的值应该是一致。

3.创建记录

user.controller文件


@ApiTags('用户')
@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @ApiOperation({ summary: '创建用户' })
  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    return new R({
      data: await this.userService.create(createUserDto),
      msg: '创建成功',
    });
  }

  @ApiOperation({ summary: '用户列表' })
  @Get()
  async findMany(@Query() pageUserDto: PageUserDto) {
    return new R({
      data: await this.userService.findMany(pageUserDto),
      msg: '获取用户列表成功',
    });
  }

  @ApiOperation({ summary: '更新用户' })
  @Put(':id')
  async update(
    @Param('id', new ParseIntPipe()) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return new R({
      data: await this.userService.update(id, updateUserDto),
      msg: '更新成功',
    });
  }

  @ApiOperation({ summary: '部分更新' })
  @Patch(':id')
  async patch(
    @Param('id', new ParseIntPipe()) id: number,
    @Body() patchUserDto: PatchUserDto,
  ) {
    return new R({
      data: await this.userService.update(id, patchUserDto),
      msg: '更新成功',
    });
  }

  @ApiOperation({ summary: '删除用户' })
  @Delete(':id')
  async remove(@Param('id', new ParseIntPipe()) id: number) {
    await this.userService.remove(id);
    return new R({
      msg: '删除成功',
    });
  }
}


user.service文件


@Injectable()
export class UserService {
  constructor(private readonly prismaService: PrismaService) {}
  async create(createUserDto: CreateUserDto) {
    const hashedPassword = await hash(createUserDto.password);
    const user = await this.prismaService.user.create({
      data: {
        ...createUserDto,
        password: hashedPassword,
      },
      omit: {
        password: true,
      },
    });
    const userVo = plainToClass(UserVo, user);
    return userVo;
  }

  async findMany(pageUserDto: PageUserDto) {
    const { page, pageSize, keywords, startTime, endTime, enabled, id } =
      pageUserDto;

    const where: Prisma.UserWhereInput = {
      deletedAt: null,
      AND: [
        {
          createdAt: {
            ...(startTime && { gte: startTime }),
            ...(endTime && { lte: endTime }),
          },
          id: {
            ...(id && { equals: id }),
          },
          enabled: {
            ...(enabled && { equals: enabled }),
          },
        },
      ],
      OR: keywords
        ? [
            {
              id: {
                equals:
                  _.toNumber(keywords) < 100000 ? _.toNumber(keywords) : 0,
              },
            },
            { username: { contains: keywords } },
          ]
        : undefined,
    };
    const records = await this.prismaService.user.findMany({
      where,

      skip: (page - 1) * pageSize,
      take: pageSize,
    });
    const total = await this.prismaService.user.count({ where });
    return plainToClass(PageUserVo, {
      records,
      total,
      page,
      pageSize,
    });
  }

  async findOneById(id: number) {
    const user = await this.prismaService.user.findUnique({
      where: {
        id,
        deletedAt: null,
      },
    });
    if (!user) {
      throw new NotFoundException(`User with id ${id} not found`);
    }
    const userVo = plainToClass(UserVo, user);
    return userVo;
  }

  async update(
    id: number,
    updateOrPatchUserDto: UpdateUserDto | PatchUserDto,
    updatedBy?: number,
  ) {
    const userVo = plainToClass(
      UserVo,
      await this.prismaService.user.update({
        where: {
          id,
          deletedAt: null,
        },
        data: {
          ...updateOrPatchUserDto,
          updatedBy,
        },
      }),
    );
    return userVo;
  }

  async remove(id: number, deletedBy?: number) {
    await this.prismaService.user.update({
      where: {
        id,
        deletedAt: null,
      },
      data: {
        deletedAt: new Date().toISOString(),
        deletedBy,
      },
    });
  }
}

image.png 操作过程中遇见了这个问题 经过找原因发现 当初定义模型的时候 username是唯一值,所有修改username的时候,要先判断 用户名是否被占用

 if (username) {
      const existingUserByUsername = await this.prismaService.user.findUnique({
        where: { username },
      });
      if (existingUserByUsername && existingUserByUsername.id !== id) {
        throw new NotFoundException(`用户名 '${username}' 已被占用.`);
      }
    }