Skip to content

Nest 与 Prisma 结合 ​

Prisma 篇 ​

注意

  1. 我这里用的版本合集
  • prisma10.2.0 版本
  • "@prisma/client": "^10.2.0",
  • "@prisma/extension-read-replicas": "^0.5.0",
  1. 这里它会比老版本多了一个 prisma.config.ts 文件

  2. 这里环境变量只能用 env 文件形式 yml 文件形式不行

新建一个 Nest 项目 ​

  • 具体的参考前面章节

安装 Prisma ​

  • 安装 Prisma CLI
bash
// 基础的使用包
pnpm install prisma@7.10

// 这个时候 他会提示 [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: @prisma/engines@7.10.0, prisma@7.10.0
// 运行 pnpm approve-builds 然后输入 a 就行

// 扩展包
pnpm install @prisma/client
// 读写分离
pnpm install @prisma/extension-read-replicas
// 数据库链接
pnpm install @prisma/adapter-mariadb

初始化 Prisma ​

bash
npx prisma init
  • 这个时候无论你.env 里面写了什么它都会给你覆盖

修改.env文件 ​

  • 这里我使用的是 mysql 数据库

  • 修改 .env, .env.development,.env.production 文件

bash

DATABASE_URL='mysql://用户名:密码@地址:端口号/数据库名'
DATABASE_USER="用户名"
DATABASE_PASSWORD="密码"
DATABASE_NAME="数据库名"
DATABASE_HOST="链接地址"
DATABASE_PORT=端口号

DATABASE_READ_URL= 'mysql://用户名:密码@地址:端口号/数据库名'
DATABASE_READ_USER="用户名"
DATABASE_READ_PASSWORD="密码"
DATABASE_READ_NAME="数据库名"
DATABASE_READ_HOST="链接地址"
DATABASE_READ_PORT=端口号

​

  • 修改 prisma/schema.prisma 文件
ts
generator client {
  provider     = "prisma-client"
  output       = "../prisma/generated/prisma"
  moduleFormat = "cjs"
}

datasource db {
  provider = "mysql"
}

修改 prisma7.config.ts 文件 ​

ts
import { defineConfig, env } from "prisma/config";
import "dotenv/config";
export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: env("DATABASE_URL"),
  },
});

拉取数据库 ​

bash
npx prisma db pull
  • 这个时候 你的 prisma/schema.prisma 文件应该会自动生成数据库的表结构

多文件处理 ​

  • 在根目录下面找到prisma文件夹

依次按照schema.prisma文件里面的内容,新建对应的文件 类似

ts
prisma
├── schema
│   ├── schema.prisma
│   ├── app.prisma
│   ├── shop.prisma
│   └── erp.prisma
  • 每一个 prisma 文件 对应的就是一张表

分出去以后 把schema.prisma 里面表的结构都删除仅保留开始的.

  • 把 schema.prisma 放到 prisma/schema 文件夹下

再次修改schema.prisma 文件 ​

ts
generator client {
  provider     = "prisma-client"
  output       = "../generated/prisma"
  moduleFormat = "cjs"
}

datasource db {
  provider = "mysql"
}

再次修改 ​

  • 把prisma文件夹放到 src 目录下面
ts
src
├── prisma
     ├──generated
     ├──schema
        ├── schema.prisma
        ├── app.prisma
        ├── shop.prisma
        └── erp.prisma
  • prisma.config.ts 文件
ts
import { defineConfig, env } from "prisma/config";
import "dotenv/config";
export default defineConfig({
  schema: "./src/prisma/schema",
  migrations: {
    path: "./src/prisma/migrations",
  },
  datasource: {
    url: env("DATABASE_URL"),
  },
});

生成 Prisma Client ​

ts
npx prisma generate

Nest 篇 ​

项目结构 ​

  • 在src目录下面新建一个prisma文件夹
bash
common
├──prismaModule
    ├── prisma.extension.service.ts  // prisma 扩展
    ├── prisma.module.ts  // prisma 模块
    ├── prisma.service.ts  // prisma 暴露出去的服务
    ├── prisma.provider.ts // 核心prisma 提供

创建 一个 Module 和 Service ​

  • 名字任意 我这里起名就是 prisma.module.ts 和 prisma.service.ts

prisma.extension.service.ts ​

  • 给 prisma 添加一个扩展方法 exists
ts
// query-helper.service.ts
import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client/extension.js";

@Injectable()
export class PrismaQueryHelperService {
  //any logic

  existsExtension = Prisma.defineExtension({
    name: "exists-extension",
    model: {
      $allModels: {
        async exists<T>(
          this: T,
          where: Prisma.Args<T, "findFirst">["where"],
        ): Promise<boolean> {
          const context = Prisma.getExtensionContext(this);
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          const result = await (context as any).findFirst({ where });
          return result !== null;
        },
      },
    },
  });
}

prisma.extension2.service.ts ​

  • 自己写的分页方法
ts
import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client/extension.js";

@Injectable()
export class PrismaQueryHelperService2 {
  existsExtension = Prisma.defineExtension({
    name: "extension2",
    model: {
      $allModels: {
        async paginate<T>(
          this: T,
          page: number,
          pageSize: number,
          options: any = {},
        ): Promise<any> {
          const context = Prisma.getExtensionContext(this);
          const skip = (page - 1) * pageSize;
          const [data, total] = await Promise.all([
            (context as any).findMany({
              skip,
              take: pageSize,
              ...options,
            }),
            (context as any).count({ where: options.where }),
          ]);
          return {
            data,
            pagination: {
              total,
              page,
              pageSize,
              totalPages: Math.ceil(total / pageSize),
              hasNextPage: page < Math.ceil(total / pageSize),
              hasPreviousPage: page > 1,
            },
          };
        },
      },
    },
  });
}

prisma.provider.ts ​

ts
import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaClient } from "../../prisma/generated/prisma/client.js";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { PrismaQueryHelperService } from "./prisma.extension.service.js";
import { PrismaQueryHelperService2 } from "./prisma.extension2.service.js";
import { readReplicas } from "@prisma/extension-read-replicas";

@Injectable()
export class PrismaProvider
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy
{
  constructor(
    private readonly configService: ConfigService,
    private readonly prismaQueryHelperService: PrismaQueryHelperService,
    private readonly prismaQueryHelperService2: PrismaQueryHelperService2,
  ) {
    // 构造函数执行时,ConfigModule 已加载完 env 文件
    super({
      adapter: new PrismaMariaDb({
        host: configService.getOrThrow<string>("DATABASE_HOST"),
        user: configService.getOrThrow<string>("DATABASE_USER"),
        password: configService.getOrThrow<string>("DATABASE_PASSWORD"),
        database: configService.getOrThrow<string>("DATABASE_NAME"),
        allowPublicKeyRetrieval: true, // 允许客户端向服务端请求 RSA 公钥
      }),
      log: ["info", "query"],
    });
  }

  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }

  withExtensions() {
    const readReplicasClient = new PrismaClient({
      adapter: new PrismaMariaDb({
        host: this.configService.getOrThrow<string>("DATABASE_READ_HOST"),
        user: this.configService.getOrThrow<string>("DATABASE_READ_USER"),
        password: this.configService.getOrThrow<string>(
          "DATABASE_READ_PASSWORD",
        ),
        database: this.configService.getOrThrow<string>("DATABASE_READ_NAME"),
        allowPublicKeyRetrieval: true, // 允许客户端向服务端请求 RSA 公钥
      }),
      log: ["info", "query"],
    });
    return this.$extends(this.prismaQueryHelperService.existsExtension)
      .$extends(readReplicas({ replicas: [readReplicasClient] }))
      .$extends(this.prismaQueryHelperService2.existsExtension);
  }
}

prisma.service.ts ​

ts
import { Inject, Injectable, OnModuleInit, Type } from "@nestjs/common";
import { PrismaProvider } from "./prisma.provider";

const ExtendedPrismaClient = class {
  constructor(provider: PrismaProvider) {
    return provider.withExtensions();
  }
} as Type<ReturnType<PrismaProvider["withExtensions"]>>;

@Injectable()
export class PrismadbService extends ExtendedPrismaClient {
  constructor(provider: PrismaProvider) {
    super(provider);
  }
}

prisma.module.ts ​

ts
import { PrismadbService } from "./prisma.service"; // 服务

import { Module, Global } from "@nestjs/common";

import { PrismaProvider } from "./prisma.provider"; // 提供

import { PrismaQueryHelperService } from "./prisma.extension.service"; // 扩展

import { PrismaQueryHelperService2 } from "./prisma.extension2.service"; // 扩展
@Global() // Global decorator to make this module available globally
@Module({
  imports: [],
  controllers: [],
  providers: [
    PrismadbService,
    PrismaProvider,
    PrismaQueryHelperService,
    PrismaQueryHelperService2,
  ],
  exports: [
    PrismadbService,
    PrismaProvider,
    PrismaQueryHelperService,
    PrismaQueryHelperService2,
  ],
})
export class PrismadbModule {}

使用 ​

app.module.ts ​

  • 重点引入 PrismadbModule 模块
ts
import { Module } from "@nestjs/common";
// 配置文件
import { ConfigModule } from "@nestjs/config";
import * as dotenv from "dotenv";
import { PrismadbModule } from "./common/prismaModule/prisma.module.js";

import { AppController } from "./app.controller.js";
import { AppService } from "./app.service.js";

const envPath = `.env.${process.env.NODE_ENV || "development"}`;

@Module({
  imports: [
    PrismadbModule,
    // 修改配置
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: envPath,
      // 这里新增.env的文件解析
      load: [() => dotenv.config({ path: ".env" })],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

控制器 ​

ts
import { Controller, Get, Post, Body } from "@nestjs/common";
import { AppService } from "./app.service.js";

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Post("getalluser")
  async getAllUser() {
    return this.appService.getAllUsers();
  }

  @Post("getpageduser")
  async getPagedUser(@Body() body: any) {
    return this.appService.getPagedUsers(body.page, body.pageSize);
  }
}

服务app.service.ts ​

ts
import { Injectable } from "@nestjs/common";

import { PrismadbService } from "./common/prismaModule/prisma.service.js";

@Injectable()
export class AppService {
  constructor(private readonly prismaService: PrismadbService) {}

  getHello(): string {
    return "Hello World!";
  }

  // 获取所有用户列表
  async getAllUsers() {
    const result = await this.prismaService.admin.findMany();
    return result.map((item) => ({
      ...item,
      id: item.id.toString(),
    }));
  }

  // 获取分页列表
  async getPagedUsers(page: number, pageSize: number) {
    const current_page = page || 1; // 当前页
    const page_size = pageSize || 10; // 每页条数
    const result = await this.prismaService.student.paginate(
      current_page,
      page_size,
      {
        where: { id: { gte: 2 } },
        orderBy: { id: "desc" },
      },
    );
    // 处理 BigInt 序列化
    if (result.data && Array.isArray(result.data)) {
      result.data = result.data.map((item: any) => ({
        ...item,
        id: item.id?.toString(),
      }));
    }
    return result;
  }
}

调用 ​

ts
POST http://localhost:5000/getalluser
Content-Type: application/json

POST http://localhost:5000/getpageduser
Content-Type: application/json

{
  "pageNum": 1,
  "pageSize": 10
}