初识 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 哪些迁移还没处理,保证数据库的一致性。

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

相关推荐
新知图书21 分钟前
ASP.NET MVC添加模型示例
数据库·oracle
gou123412341 小时前
【Golang入门】第一章:环境搭建与Hello World
开发语言·后端·golang
酷爱码1 小时前
SpringBoot整合Sa-Token实现RBAC权限模型的过程解析
数据库·spring boot·后端
寻月隐君2 小时前
Web3 开发实操:用 Anchor 在 Solana 创建代币 Mint Account
后端·web3·github
爱上语文2 小时前
MyBatisPlus(1):快速入门
java·开发语言·数据库·后端·mybatis
marsjin3 小时前
如何使用Python从MySQL数据库导出表结构到Word文档
数据库·python·mysql
Aspirin_Slash4 小时前
【Tauri2.0教程(九)】日志插件的使用
后端
David爱编程4 小时前
轻量容器如何改变开发世界?Docker 基本概念与架构详解
后端·docker·容器
FogLetter4 小时前
Node.js与OpenAI的完美融合:打造你的AI驱动型后端应用 🚀
后端·aigc
程序员爱钓鱼4 小时前
Go语言之接口与多态 -《Go语言实战指南》
后端·go·google io