Prisma Prisma

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

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 示例。

入门

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

注意

如果您已经有现有项目并考虑迁移到 Prisma,您可以按照 将 Prisma 添加到现有项目 的指南进行操作。如果您要从 TypeORM 迁移,您可以阅读指南 从 TypeORM 迁移到 Prisma

创建您的 NestJS 项目

要开始使用,请安装 NestJS CLI 并使用以下命令创建您的应用程序骨架:

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

请参阅 第一步 页面,了解有关此命令创建的项目文件的更多信息。另请注意,您现在可以运行 npm start 来启动您的应用程序。在 http://localhost:3000/ 运行的 REST API 当前提供在 src/app.controller.ts 中实现的单个路由。在本指南的过程中,您将实现其他路由来存储和检索有关 usersposts 的数据。

设置 Prisma

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

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

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

bash
$ npx prisma

如果您使用的是 Yarn,则可以按如下方式安装 Prisma CLI:

bash
$ yarn add prisma --dev

安装后,您可以通过在 CLI 前添加 yarn 来调用它:

bash
$ yarn prisma

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

bash
$ npx prisma init

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

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

设置数据库连接

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

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

generator client {
  provider = "prisma-client-js"
}

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

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

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

SQLite 数据库是简单文件;使用 SQLite 数据库不需要服务器。因此,您无需使用 hostport 配置连接 URL,只需将其指向本地文件(在本例中称为 dev.db)。此文件将在下一步中创建。

如果您使用的是 PostgreSQL 或 MySQL,请展开
详情

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

在本节中,您将使用 Prisma Migrate 在数据库中创建两个新表。Prisma Migrate 会为 Prisma 架构中的声明性数据模型定义生成 SQL 迁移文件。这些迁移文件完全可自定义,因此您可以配置底层数据库的任何其他功能或包含其他命令,例如用于播种的命令。

将以下两个模型添加到您的 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?
}

有了 Prisma 模型后,您可以生成 SQL 迁移文件并针对数据库运行它们。在终端中运行以下命令:

bash
$ npx prisma migrate dev --name init

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

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

安装并生成 Prisma 客户端

Prisma 客户端是一种类型安全的数据库客户端,它是从您的 Prisma 模型定义中生成的。由于采用了这种方法,Prisma 客户端可以公开专门针对您的模型量身定制的 CRUD 操作。

要在您的项目中安装 Prisma 客户端,请在您的终端中运行以下命令:

bash
$ npm install @prisma/client

请注意,在安装过程中,Prisma 会自动为您调用 prisma generate 命令。将来,您需要在每次更改 Prisma 模型后运行此命令以更新生成的 Prisma 客户端。

Note

prisma generate 命令读取你的 Prisma 模式并更新 node_modules/@prisma/client 内生成的 Prisma 客户端库。

在 NestJS 服务中使用 Prisma Client

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

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

src目录中,创建一个名为prisma.service.ts的新文件,并向其中添加以下代码:

ts
import { Injectable, OnModuleInit } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

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

onModuleInit 是可选的 — 如果您忽略它,Prisma 将在第一次调用数据库时延迟连接。

接下来,您可以编写服务,用于从 Prisma 模式对 UserPost 模型进行数据库调用。

仍然在 src 目录中,创建一个名为 user.service.ts 的新文件,并向其中添加以下代码:

ts
import { Injectable } from '@nestjs/common'
import { Prisma, User } from '@prisma/client'
import { PrismaService } from './prisma.service'

@Injectable()
export class UserService {
  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 的新文件,并向其中添加以下代码:

ts
import { Injectable } from '@nestjs/common'
import { Post, Prisma } from '@prisma/client'
import { PrismaService } from './prisma.service'

@Injectable()
export class PostService {
  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,
    })
  }
}

您的 UserServicePostService 当前包装了 Prisma Client 中可用的 CRUD 查询。在实际应用程序中,服务也是向您的应用程序添加业务逻辑的地方。例如,您可以在 UserService 内部使用一个名为 updatePassword 的方法,该方法负责更新用户的密码。

请记住在应用程序模块中注册新服务。

在主应用程序控制器中实现您的 REST API 路由

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

app.controller.ts 文件的内容替换为以下代码:

ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
} from '@nestjs/common'
import { Post as PostModel, User as UserModel } from '@prisma/client'
import { UserService } from './user.service'
import { PostService } from './post.service'

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

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

This controller implements the following routes:

GET

  • /post/:id: Fetch a single post by its id
  • /feed: Fetch all published posts
  • /filter-posts/:searchString: Filter posts by title or content

POST

  • /post: Create a new post
    • Body:
      • title: String (required): The title of the post
      • content: String (optional): The content of the post
      • authorEmail: String (required): The email of the user that creates the post
  • /user: Create a new user
    • Body:
      • email: String (required): The email address of the user
      • name: String (optional): The name of the user

PUT

  • /publish/:id: Publish a post by its id

DELETE

  • /post/:id: Delete a post by its id

Summary

在本秘籍中,您学习了如何使用 Prisma 和 NestJS 来实现 REST API。实现 API 路由的控制器正在调用PrismaService,后者又使用 Prisma Client 向数据库发送查询以满足传入请求的数据需求。

如果您想了解有关使用 NestJS 和 Prisma 的更多信息,请务必查看以下资源: