Prisma

Prisma 是一个用于 Node.js 和 TypeScript 的开源 ORM。它被用作编写纯 SQL 或使用其他数据库访问工具(如 SQL 查询构建器(如 knex.js)或 ORM(如 TypeORMSequelize))的替代方案。Prisma 目前支持 PostgreSQL、MySQL、SQL Server、SQLite、MongoDB 和 CockroachDB(预览版)。

虽然 Prisma 可以与纯 JavaScript 一起使用,但它拥抱 TypeScript,并提供了超越 TypeScript 生态系统中其他 ORM 保证的类型安全级别。你可以在这里找到 Prisma 和 TypeORM 类型安全保证的深入比较。

注意

如果你想快速了解 Prisma 的工作原理,可以遵循快速入门或阅读文档中的介绍。在 prisma-examples 仓库中还有RESTGraphQL 的可运行示例。

入门

在本指南中,你将学习如何从头开始使用 NestJS 和 Prisma。你将构建一个示例 NestJS 应用程序,该应用程序具有可以在数据库中读取和写入数据的 REST API。

出于本指南的目的,你将使用 SQLite 数据库,以节省设置数据库服务器的开销。请注意,即使你使用的是 PostgreSQL 或 MySQL,你仍然可以遵循本指南——你将在适当的位置获得使用这些数据库的额外说明。

注意

如果你已经有一个现有项目并考虑迁移到 Prisma,可以遵循将 Prisma 添加到现有项目的指南。如果你从 TypeORM 迁移,可以阅读从 TypeORM 迁移到 Prisma 的指南。

创建 NestJS 项目

首先,安装 NestJS CLI 并使用以下命令创建你的应用程序骨架:

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

请参阅入门页面,了解此命令创建的项目文件的更多信息。还要注意,你现在可以运行 npm start 来启动你的应用程序。运行在 http://localhost:3000/ 的 REST API 目前提供一个在 src/app.controller.ts 中实现的路由。在本指南的过程中,你将实现额外的路由来存储和检索关于_用户_和_文章_的数据。

设置 Prisma

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

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

在接下来的步骤中,我们将使用 Prisma CLI。作为最佳实践,建议通过在前面加上 npx 来本地调用 CLI:

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

如果你使用 Yarn,那么你可以按如下方式安装 Prisma CLI:

$ yarn add prisma --dev

安装后,你可以通过在前面加上 yarn 来调用它:

$ yarn prisma

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

$ npx prisma init

此命令创建一个新的 prisma 目录,其中包含以下内容:

  • schema.prisma:指定你的数据库连接并包含数据库架构
  • prisma.config.ts:你的项目的配置文件
  • .env:一个 dotenv 文件,通常用于在一组环境变量中存储你的数据库凭据

设置生成器输出路径

通过在 prisma init 期间传递 --output ../src/generated/prisma,或直接在你的 Prisma 架构中指定生成的 Prisma 客户端的输出 path

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

配置模块格式

在生成器中将 moduleFormat 设置为 cjs

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

需要 moduleFormat 配置,因为 Prisma v7 默认作为 ES 模块发布,这与 NestJS 的 CommonJS 设置不兼容。将 moduleFormat 设置为 cjs 会强制 Prisma 生成 CommonJS 模块而不是 ESM。

设置数据库连接

你的数据库连接在 schema.prisma 文件的 datasource 块中配置。默认情况下它设置为 postgresql但由于你在本指南中使用 SQLite 数据库,你需要将 datasource 块的 provider 字段调整为 sqlite

datasource db {
  provider = "sqlite"
}

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

现在,打开 .env 并将 DATABASE_URL 环境变量调整如下:

DATABASE_URL="file:./dev.db"

确保你已配置 ConfigModule,否则 DATABASE_URL 变量将不会从 .env 中获取。

SQLite 数据库是简单的文件;使用 SQLite 数据库不需要服务器。因此,你不需要配置带有_主机_和_端口_的连接 URL,只需将其指向一个本地文件,在本例中称为 dev.db。此文件将在下一步中创建。

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

使用 PostgreSQL 和 MySQL,你需要配置连接 URL 以指向_数据库服务器_。你可以在这里了解更多关于所需连接 URL 格式的信息。

PostgreSQL

如果你使用 PostgreSQL,你需要按如下方式调整 schema.prisma.env 文件:

schema.prisma

datasource db {
  provider = "postgresql"
}

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

.env

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

将所有大写字母拼写的占位符替换为你的数据库凭据。请注意,如果你不确定 SCHEMA 占位符应该提供什么,它很可能是默认值 public

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

如果你想了解如何设置 PostgreSQL 数据库,可以遵循这篇关于在 Heroku 上设置免费 PostgreSQL 数据库的指南。

MySQL

如果你使用 MySQL,你需要按如下方式调整 schema.prisma.env 文件:

schema.prisma

datasource db {
  provider = "mysql"
}

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

.env

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

datasource db {
  provider = "sqlserver"
}

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

.env

将所有大写字母拼写的占位符替换为你的数据库凭据。请注意,如果你不确定 encrypt 占位符应该提供什么,它很可能是默认值 true

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

使用 Prisma Migrate 创建两个数据库表

在本节中,你将使用 Prisma Migrate 在数据库中创建两个新表。Prisma Migrate 为 Prisma 架构中的声明式数据模型定义生成 SQL 迁移文件。这些迁移文件完全可定制,以便你可以配置底层数据库的任何附加功能或包含附加命令,例如用于种子数据。

将以下两个模型添加到你的 schema.prisma 文件中:

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 模型,你可以生成 SQL 迁移文件并针对数据库运行它们。在终端中运行以下命令:

$ npx prisma migrate dev --name init

prisma migrate dev 命令生成 SQL 文件并直接针对数据库运行它们。在这种情况下,在现有的 prisma 目录中创建了以下迁移文件:

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

在你的 SQLite 数据库中创建了以下表:

-- 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,请在终端中运行以下命令:

$ npm install @prisma/client

安装后,你可以运行 generate 命令来生成项目所需的类型和客户端。如果对架构进行了任何更改,你需要重新运行 generate 命令以保持这些类型同步。

$ npx prisma generate

除了 Prisma Client,你还需要为你正在使用的数据库类型安装驱动程序适配器。对于 SQLite,你可以安装 @prisma/adapter-better-sqlite3 驱动程序。

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

在 NestJS 服务中使用 Prisma Client

你现在可以使用 Prisma Client 发送数据库查询。如果你想了解更多关于使用 Prisma Client 构建查询的信息,请查看 API 文档

在设置 NestJS 应用程序时,你需要将 Prisma Client API 用于数据库查询抽象到服务中。首先,你可以创建一个新的 PrismaService,负责实例化 PrismaClient 并连接到你的数据库。

src 目录中,创建一个名为 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 架构中对 UserPost 模型进行数据库调用。

仍在 src 目录中,创建一个名为 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 生成的类型来确保服务暴露的方法正确类型化的。因此,你节省了为模型类型化和创建额外接口或 DTO 文件的样板代码。

现在对 Post 模型做同样的操作。

仍在 src 目录中,创建一个名为 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,
    });
  }
}

你的 UsersServicePostsService 目前包装了 Prisma Client 中可用的 CRUD 查询。在实际应用程序中,服务也是向应用程序添加业务逻辑的地方。例如,你可以在 UsersService 中有一个名为 updatePassword 的方法,负责更新用户的密码。

记得在 app 模块中注册新服务。

在主 app 控制器中实现 REST API 路由

最后,你将使用你在前面章节中创建的服务来实现应用程序的不同路由。出于本指南的目的,你将把所有路由放入已经存在的 AppController 类中。

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

此控制器实现以下路由:

GET
  • /post/:id:通过 id 获取单个文章
  • /feed:获取所有_已发布_的文章
  • /filter-posts/:searchString:按 titlecontent 过滤文章
POST
  • /post:创建新文章
    • 正文:
      • title: String(必需):文章标题
      • content: String(可选):文章内容
      • authorEmail: String(必需):创建文章的用户的电子邮件
  • /user:创建新用户
    • 正文:
      • email: String(必需):用户的电子邮件地址
      • name: String(可选):用户名
PUT
  • /publish/:id:通过 id 发布文章
DELETE
  • /post/:id:通过 id 删除文章

总结

在本指南中,你学习了如何将 Prisma 与 NestJS 一起使用来实现 REST API。实现 API 路由的控制器调用 PrismaService,后者又使用 Prisma Client 向数据库发送查询以满足传入请求的数据需求。

如果你想了解更多关于在 NestJS 中使用 Prisma 的信息,请务必查看以下资源: