初识 Prisma-结合NestJS

nest-prisma

以前一直使用的是 typeorm 搭配 nestjs,但是 typeorm 有一些缺点,比如:

  • 复杂查询需依赖 createQueryBuilder,代码很长很难写
  • findOne有时出错后会返回第一条记录,这个有时候很难注意到
  • 迁移发版的时候风险很大,容易丢失数据,因为都是直接同步模型的变化
  • 社区活跃度下降,更新频率较低,基本没有啥更新了

所以想尝试下别的 ORM,Prisma 是一个不错的选择,用的人比较多:是一个新一代的 Node.js 和 TypeScript ORM,它通过直观的数据模型、自动化迁移、类型安全和自动补全,在数据库操作方面解锁了全新的开发者体验。支持大多数的主流数据库,包括 PostgreSQL、MySQL、SQLite 和 MongoDB。

安装 prisma

首先用 @nestjs/cli 创建一个新的 nestjs 项目后,接着添加 prisma

bash 复制代码
npm install prisma --save-dev

接着运行 prisma 初始化

bash 复制代码
npx prisma init

这会生成两个文件

  • prisma/schema.prisma // 数据库模型
  • .env // 数据库连接信息,这个文件会被 git 忽略,属于本地的文件,不上传到 git 仓库,各个不同的环境下的数据库连接信息需要自己配置

.env 文件

bash 复制代码
DATABASE_URL="postgresql://hezf:@localhost:5432/prisma-pro?schema=public"

prisma/schema.prisma 文件

bash 复制代码
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

prisma model

接下来定义数据库模型

bash 复制代码
model User {
  id    Int     @default(autoincrement()) @id
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int      @default(autoincrement()) @id
  title     String
  content   String?
  published Boolean? @default(false)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}

现在定义完了数据库模型,我们可以使用 prisma 命令来生成数据库模型

bash 复制代码
npx prisma migrate dev --name init

这时候会生成一个迁移的 sql 文件

bash 复制代码
prisma
├── migrations
│   └── 20250411005120_init
│       └── migration.sql
└── schema.prisma

检查数据库,会发现已经生成了对应的表

prisma-client

接下来我们通过 prisma-client 来操作数据库

bash 复制代码
npm install @prisma/client

继续创建一个文件 src/prisma.service.ts

bash 复制代码
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}

然后在 src/app.module.ts 中引入 PrismaService;接下来创建一个 src/user.service.ts 文件

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { User, Prisma } from '@prisma/client';

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async user(
    userWhereUniqueInput: Prisma.UserWhereUniqueInput,
  ): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: userWhereUniqueInput,
    });
  }

  async users(params: {
    skip?: number;
    take?: number;
    cursor?: Prisma.UserWhereUniqueInput;
    where?: Prisma.UserWhereInput;
    orderBy?: Prisma.UserOrderByWithRelationInput;
  }): Promise<User[]> {
    const { skip, take, cursor, where, orderBy } = params;
    return this.prisma.user.findMany({
      skip,
      take,
      cursor,
      where,
      orderBy,
    });
  }

  async createUser(data: Prisma.UserCreateInput): Promise<User> {
    return this.prisma.user.create({
      data,
    });
  }

  async updateUser(params: {
    where: Prisma.UserWhereUniqueInput;
    data: Prisma.UserUpdateInput;
  }): Promise<User> {
    const { where, data } = params;
    return this.prisma.user.update({
      data,
      where,
    });
  }

  async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
    return this.prisma.user.delete({
      where,
    });
  }
}

接着创建 post.service.ts 文件

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Post, Prisma } from '@prisma/client';

@Injectable()
export class PostsService {
  constructor(private prisma: PrismaService) {}

  async post(
    postWhereUniqueInput: Prisma.PostWhereUniqueInput,
  ): Promise<Post | null> {
    return this.prisma.post.findUnique({
      where: postWhereUniqueInput,
    });
  }

  async posts(params: {
    skip?: number;
    take?: number;
    cursor?: Prisma.PostWhereUniqueInput;
    where?: Prisma.PostWhereInput;
    orderBy?: Prisma.PostOrderByWithRelationInput;
  }): Promise<Post[]> {
    const { skip, take, cursor, where, orderBy } = params;
    return this.prisma.post.findMany({
      skip,
      take,
      cursor,
      where,
      orderBy,
    });
  }

  async createPost(data: Prisma.PostCreateInput): Promise<Post> {
    return this.prisma.post.create({
      data,
    });
  }

  async updatePost(params: {
    where: Prisma.PostWhereUniqueInput;
    data: Prisma.PostUpdateInput;
  }): Promise<Post> {
    const { data, where } = params;
    return this.prisma.post.update({
      data,
      where,
    });
  }

  async deletePost(where: Prisma.PostWhereUniqueInput): Promise<Post> {
    return this.prisma.post.delete({
      where,
    });
  }
}

src/user.service.tssrc/post.service.ts 引入到 src/app.module.ts 中后,继续 app.controller.ts:

typescript 复制代码
import {
  Controller,
  Get,
  Param,
  Post,
  Body,
  Put,
  Delete,
} from '@nestjs/common';
import { UsersService } from './user.service';
import { PostsService } from './post.service';
import { User as UserModel, Post as PostModel } from '@prisma/client';

@Controller()
export class AppController {
  constructor(
    private readonly userService: UsersService,
    private readonly postService: PostsService,
  ) {}

  @Get('post/:id')
  async getPostById(@Param('id') id: string): Promise<PostModel> {
    return this.postService.post({ id: Number(id) });
  }

  @Get('feed')
  async getPublishedPosts(): Promise<PostModel[]> {
    return this.postService.posts({
      where: { published: true },
    });
  }

  @Get('filtered-posts/:searchString')
  async getFilteredPosts(
    @Param('searchString') searchString: string,
  ): Promise<PostModel[]> {
    return this.postService.posts({
      where: {
        OR: [
          {
            title: { contains: searchString },
          },
          {
            content: { contains: searchString },
          },
        ],
      },
    });
  }

  @Post('post')
  async createDraft(
    @Body() postData: { title: string; content?: string; authorEmail: string },
  ): Promise<PostModel> {
    const { title, content, authorEmail } = postData;
    return this.postService.createPost({
      title,
      content,
      author: {
        connect: { email: authorEmail },
      },
    });
  }

  @Post('user')
  async signupUser(
    @Body() userData: { name?: string; email: string },
  ): Promise<UserModel> {
    return this.userService.createUser(userData);
  }

  @Put('publish/:id')
  async publishPost(@Param('id') id: string): Promise<PostModel> {
    return this.postService.updatePost({
      where: { id: Number(id) },
      data: { published: true },
    });
  }

  @Delete('post/:id')
  async deletePost(@Param('id') id: string): Promise<PostModel> {
    return this.postService.deletePost({ id: Number(id) });
  }
}

以上就完成了一个简单的 nestjs 项目,这里创建了两个服务 user.service.tspost.service.ts,可以在 controller 层调用的更舒心一些,和 nestjs 的范式一致。

迁移和发版

上面我们在运行 npx prisma migrate dev --name init的时候,会生成一个迁移的 sql 文件,这个文件是用来记录数据库的迁移历史的,我们可以通过这个文件来回滚数据库。

在日常开发中,我们可能会修改数据库模型,比如给 user 添加一个电话号码的字段:

typescript 复制代码
model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  tel   String?
  posts Post[]
}

修改完之后,本地运行这个项目,这个时候我们发现,只修改这里没有用,数据库的结构没有变化。需要重新生成迁移文件,然后运行迁移文件,这个时候我们可以使用以下命令:

bash 复制代码
npx prisma migrate dev --name add-tel

运行之后我们会发现,数据库的结构已经变化了,并且生成了一个新的迁移文件 prisma/migrations/20250411021730_add_tel,里面的内容是:

sql 复制代码
-- AlterTable
ALTER TABLE "User" ADD COLUMN     "tel" TEXT;

此时再来看看引入的 import { User, Prisma } from '@prisma/client'; ,会发现 User 模型中已经有了 tel 字段了,在运行上面的命令之前是没有的;

这里有一个问题,以上的改动都是在本地 node_modules 目录下的 index.d.ts体现的,相当于修改了本地的文件,不过这份文件不会被 git 跟踪,所以不会被提交到 git 仓库。发版的时候怎么办呢?本地开发环境的node_modules 目录下的 index.d.ts和目标环境的node_modules 目录下的 index.d.ts是不一致的,所以我们需要手动同步一下。

发版的时候,需要执行以下命令去迁移合并:

bash 复制代码
npx prisma migrate deploy
## 注意:迁移部署通常应该是自动化CI/CD管道的一部分,我们不建议在本地运行此命令以将更改部署到生产数据库。

这样就可以完成模型变化的迁移了

数据库的迁移历史

在我们第一次运行 npx prisma migrate dev --name init的时候,在数据库除了会生成对应的 model 的表结构,还有一个 _prisma_migrations 表,这个表是用来记录数据库的迁移历史的。打开之后,里面记录着所有的迁移历史,包括第一次的 init 迁移和后续的 add-tel 迁移。这样就能保证数据库的一致性,知道哪些迁移是新增的,哪些是已经存在的。这在我们发版的时候非常有用,生产环境的数据库会告诉 Prisma 哪些迁移还没处理,保证数据库的一致性。

完整源码在这里,可以下载下来使用。

相关推荐
magic 24512 分钟前
SpringMVC——第三章:获取请求数据
java·数据库·springmvc
偶尔微微一笑18 分钟前
postgresql数据库基本操作
运维·数据库·postgresql·oracle
o不ok!21 分钟前
实验4 mySQL查询和视图
数据库·mysql·oracle
有时间要学习39 分钟前
MySQL——数据库基础&&操作
数据库·mysql
临界点oc1 小时前
Redis从入门到实战——实战篇(下)
数据库·redis·缓存
Dreams_l1 小时前
MySQL初阶:数据库基础,数据库和表操作,数据库中的数据类型
数据库·mysql
陳長生.1 小时前
数据库Mysql_联合查询
数据库·mysql
小杜-coding2 小时前
黑马点评day02(缓存)
java·spring boot·redis·后端·spring·maven·mybatis
_extraordinary_2 小时前
MySQL基本查询(二)
数据库·mysql
橙色小博2 小时前
Python中的JSON库,详细介绍与代码示例
数据库·python·json