Skip to content

Prisma ​

Prisma 是一个面向 Node.js 与 TypeScript 的开源 ORM。它可以作为手写 SQL,或其他数据库访问工具(如 SQL 查询构建器 knex.js,以及 ORM TypeORM 与 Sequelize)的替代方案。Prisma 当前支持 PostgreSQL、MySQL、SQL Server、SQLite、MongoDB 和 CockroachDB(预览)。

虽然 Prisma 也可以与原生 JavaScript 一起使用,但它非常拥抱 TypeScript,并提供了超出 TypeScript 生态中其他 ORM 的类型安全能力。你可以在这里查看 Prisma 与 TypeORM 在类型安全方面的深入对比。

注意

如果你想快速了解 Prisma 的工作方式,可以查看 Quickstart,或者阅读官方文档中的 Introduction。prisma-examples 仓库中也提供了可直接运行的 REST 和 GraphQL 示例。

开始使用 ​

在这篇秘籍中,你将从零开始学习如何在 NestJS 中使用 Prisma。你将构建一个带 REST API 的示例 NestJS 应用,并让它能够读写数据库中的数据。

为了简化演示,你会使用 SQLite 数据库,这样就不需要额外搭建数据库服务器。不过请注意,即使你使用的是 PostgreSQL 或 MySQL,也同样可以跟着本指南进行,只是在对应步骤中需要使用额外说明。

注意

如果你已经有一个现成项目,并考虑迁移到 Prisma,可以参考这份将 Prisma 加入现有项目的指南。如果你是从 TypeORM 迁移而来,也可以阅读从 TypeORM 迁移到 Prisma。

创建 NestJS 项目 ​

首先,安装 NestJS CLI,并通过以下命令创建项目骨架:

bash
$ npm install -g @nestjs/cli
$ nest new hello-prisma

关于该命令生成的项目文件,请参见 First steps。你现在也可以运行 npm start 来启动应用。当前运行在 http://localhost:3000/ 的 REST API 只暴露了一个由 src/app.controller.ts 实现的路由。在本指南中,你将继续为它添加用于存储和获取 users 与 posts 数据的路由。

设置 Prisma ​

先在项目中将 Prisma CLI 安装为开发依赖:

bash
$ cd hello-prisma
$ npm install prisma --save-dev

接下来的步骤中,我们会使用 Prisma CLI。最佳实践是通过 npx 以前缀的方式在本地调用 CLI:

bash
$ npx prisma
如果你使用的是 Yarn,请展开

如果你使用 Yarn,那么可以这样安装 Prisma CLI:

bash
$ yarn add prisma --dev

安装完成后,可以通过 yarn 前缀调用它:

bash
$ yarn prisma

现在,使用 Prisma CLI 的 init 命令创建初始 Prisma 配置:

bash
$ npx prisma init

这个命令会创建一个新的 prisma 目录,内容如下:

  • schema.prisma:指定数据库连接并包含数据库 schema
  • prisma.config.ts:项目配置文件
  • .env:一个 dotenv 文件,通常用于存放数据库凭证等环境变量

设置生成器输出路径 ​

你可以在执行 prisma init 时通过传入 --output ../src/generated/prisma 来指定生成的 Prisma Client 输出 path,也可以直接在 Prisma schema 中这样写:

groovy
generator client {
  provider        = "prisma-client"
  output          = "../src/generated/prisma"
}

配置模块格式 ​

将生成器中的 moduleFormat 设置为 cjs:

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

注意

之所以必须配置 moduleFormat,是因为 Prisma v7 默认以 ES module 形式发布,而这与 NestJS 默认的 CommonJS 配置不兼容。将 moduleFormat 设置为 cjs 可以强制 Prisma 生成 CommonJS 模块,而不是 ESM。

设置数据库连接 ​

数据库连接配置位于 schema.prisma 文件中的 datasource 块。默认它会被设置为 postgresql,但由于本指南中使用的是 SQLite,因此你需要把 datasource 块中的 provider 字段调整为 sqlite:

groovy
datasource db {
  provider = "sqlite"
}

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

接着,打开 .env,把 DATABASE_URL 环境变量改成下面这样:

bash
DATABASE_URL="file:./dev.db"

请确保你已经配置了 ConfigModule,否则 DATABASE_URL 将无法从 .env 中被读取。

SQLite 数据库本质上只是一个文件;使用 SQLite 不需要数据库服务器。因此,与其配置带有 host 和 port 的连接 URL,不如直接指向一个本地文件,这里是 dev.db。这个文件会在下一步创建出来。

如果你使用的是 PostgreSQL、MySQL、MsSQL 或 Azure SQL,请展开

对于 PostgreSQL 和 MySQL,你需要把连接 URL 配置为指向数据库服务器。你可以在这里了解所需连接 URL 的格式。

PostgreSQL

如果你使用 PostgreSQL,需要将 schema.prisma 和 .env 文件调整如下:

schema.prisma

groovy
datasource db {
  provider = "postgresql"
}

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

.env

bash
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA"

将所有大写占位符替换为你的数据库凭证。如果你不确定 SCHEMA 应该填什么,大多数情况下它就是默认值 public:

bash
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public"

如果你想了解如何搭建 PostgreSQL 数据库,可以参考这篇指南:在 Heroku 上搭建免费的 PostgreSQL 数据库。

MySQL

如果你使用 MySQL,需要将 schema.prisma 和 .env 文件调整如下:

schema.prisma

groovy
datasource db {
  provider = "mysql"
}

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

.env

bash
DATABASE_URL="mysql://USER:PASSWORD@HOST:PORT/DATABASE"

将所有大写占位符替换为你的数据库凭证。

Microsoft SQL Server / Azure SQL Server

如果你使用 Microsoft SQL Server 或 Azure SQL Server,需要将 schema.prisma 和 .env 文件调整如下:

schema.prisma

groovy
datasource db {
  provider = "sqlserver"
}

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

.env

将所有大写占位符替换为你的数据库凭证。如果你不确定 encrypt 占位符应该填什么,大多数情况下默认值 true 就可以:

bash
DATABASE_URL="sqlserver://HOST:PORT;database=DATABASE;user=USER;password=PASSWORD;encrypt=true"

使用 Prisma Migrate 创建两张数据库表 ​

本节中,你将通过 Prisma Migrate 在数据库中创建两张新表。Prisma Migrate 会根据 Prisma schema 中声明式的数据模型生成 SQL migration 文件。这些 migration 文件是完全可定制的,因此你可以针对底层数据库加入额外特性或自定义命令,例如数据种子逻辑。

将下面两个模型加入 schema.prisma 文件:

groovy
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?
}

模型定义好后,就可以生成 SQL migration 文件并将其应用到数据库中。请在终端执行:

bash
$ npx prisma migrate dev --name init

这个 prisma migrate dev 命令会生成 SQL 文件,并直接把它们运行到数据库中。本例中,它会在现有的 prisma 目录里创建如下 migration 文件:

bash
$ tree prisma
prisma
├── dev.db
├── migrations
│   └── 20201207100915_init
│       └── migration.sql
└── schema.prisma
展开查看生成的 SQL 语句

下面这些表会被创建到你的 SQLite 数据库中:

sql
-- CreateTable
CREATE TABLE "User" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "email" TEXT NOT NULL,
    "name" TEXT
);

-- CreateTable
CREATE TABLE "Post" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title" TEXT NOT NULL,
    "content" TEXT,
    "published" BOOLEAN DEFAULT false,
    "authorId" INTEGER,

    FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");

安装并生成 Prisma Client ​

Prisma Client 是一个类型安全的数据库客户端,它是根据 Prisma 模型定义_生成_出来的。正因为采用了这种方式,Prisma Client 可以暴露出专门针对你的模型定制的 CRUD 操作。

要在项目中安装 Prisma Client,请在终端中执行:

bash
$ npm install @prisma/client

安装完成后,你可以运行 generate 命令来生成项目所需的类型与 Client。只要 schema 发生变化,你都需要重新执行 generate,以保持这些类型同步。

bash
$ npx prisma generate

除了 Prisma Client 之外,你还需要安装一个与你所使用数据库类型对应的驱动适配器。对于 SQLite,可以安装 @prisma/adapter-better-sqlite3:

bash
npm install @prisma/adapter-better-sqlite3
如果你使用的是 PostgreSQL、MySQL、MsSQL 或 AzureSQL,请展开
  • 对于 PostgreSQL
bash
npm install @prisma/adapter-pg
  • 对于 MySQL、MsSQL、AzureSQL:
bash
npm install @prisma/adapter-mariadb

在 NestJS 服务中使用 Prisma Client ​

现在你已经可以使用 Prisma Client 向数据库发送查询了。如果你想进一步了解如何使用 Prisma Client 构建查询,请查看 API 文档。

在搭建 NestJS 应用时,通常你会希望把 Prisma Client 的数据库查询能力封装进一个 service 中。首先可以创建一个新的 PrismaService,负责实例化 PrismaClient 并连接数据库。

在 src 目录中创建一个名为 prisma.service.ts 的新文件,并加入如下代码:

typescript
// prisma.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaClient } from './generated/prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';

@Injectable()
export class PrismaService extends PrismaClient {
  constructor() {
    const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL });
    super({ adapter });
  }
}

接下来,你可以为 Prisma schema 中的 User 和 Post 模型编写服务,以便执行数据库调用。

仍然在 src 目录中,创建一个新的 user.service.ts 文件,并加入如下代码:

typescript
// user.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { User, Prisma } from 'generated/prisma';

@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,
    });
  }
}

请注意,这里你直接使用了 Prisma Client 生成出的类型,从而确保 service 暴露的方法具备正确类型。这样你就省去了自己为模型定义类型、额外创建接口或 DTO 文件的样板工作。

现在,对 Post 模型也执行同样的操作。

仍然在 src 目录中,创建一个新的 post.service.ts 文件,并加入如下代码:

typescript
// post.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Post, Prisma } from 'generated/prisma';

@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,
    });
  }
}

目前你的 UsersService 和 PostsService 只是对 Prisma Client 中的 CRUD 查询做了一层封装。在真实项目中,service 同时也是承载业务逻辑的地方。比如,你可以在 UsersService 中定义一个 updatePassword 方法,专门负责更新用户密码。

记得把这些新服务注册到应用模块中。

在主应用控制器中实现 REST API 路由 ​

最后,你会使用前面创建的服务来实现应用中的不同路由。为了简化本指南,我们把所有路由都放在现有的 AppController 类中。

将 app.controller.ts 文件内容替换为:

typescript
// app.controller.ts
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 'generated/prisma';

@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) });
  }
}

这个 controller 实现了以下路由:

GET ​

  • /post/:id:根据 id 获取单篇 post
  • /feed:获取所有已发布的 posts
  • /filter-posts/:searchString:按 title 或 content 过滤 posts

POST ​

  • /post:创建一个新 post
    • Body:
      • title: String(必填):post 标题
      • content: String(可选):post 内容
      • authorEmail: String(必填):创建该 post 的用户邮箱
  • /user:创建一个新用户
    • Body:
      • email: String(必填):用户邮箱地址
      • name: String(可选):用户名

PUT ​

  • /publish/:id:根据 id 发布一个 post

DELETE ​

  • /post/:id:根据 id 删除一个 post

总结 ​

在这篇秘籍中,你学习了如何将 Prisma 与 NestJS 结合使用来实现一个 REST API。负责 API 路由的 controller 会调用 PrismaService,后者再利用 Prisma Client 向数据库发送查询,从而满足传入请求的数据需求。

如果你想进一步了解如何在 NestJS 中使用 Prisma,建议继续查看以下资源:

基于 NestJS 官方文档翻译