NestJS12-Prisma

966 阅读8分钟

Prisma是一个开源Node.js的ORM且使用TypeScript。它被用作编写普通SQL或使用其他数据库访问工具的替代方案,如SQL查询生成器(如knex.js)或ORM(如TypeORMSequelize),Prisma现在支持SQL, MySQL, SQL Server, SQLite, MongoDB 和 CockroachDB (预览)。

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

  • 注意

如果您想要快速的知道怎么让Prisma工作,你可以查看Quickstart或者阅读文档里面的介绍.这里也有一些已经做好的RESTGraphQL例子放在prisma-examples中。

开始

在这篇文章中,您将会学到怎么在NestJS中使用Prisma.您要构建一个简单的NestJS应用程序,它可以向数据库读写数据。

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

  • 注意

如果您已经有了一个存在的项目并且考虑把它移植到Prisma中,您可以阅读这篇文章,如果您想从typeORM移植过来您可以阅读这篇文章

创建您的NestJS项目

在开始之前,安装NestJS CLI 并通过下面的命令来创建项目的骨架

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

请查看官方的第一步来学习它的一些基本命令。您也可以现在运行npm start来启动您的应用。REST API就会在http://localhost:3000这个地址运行,它提供一个单一的路由在src/app.controller.ts中实现。等读完这篇指导,您将会添加新的路由它实现了关于usersposts的数据存取。

设定Prisma

首先要在您的项目中安装Prisma CLI依赖

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

接下来的步骤中,我将会利用Prisma CLI.作为最佳实践,推荐大家运行CLI的时候加上前缀npx

$ npx prisma
扩展:如果您使用Yarn 如果您使用Yarn,您可以按照下面的方法进行安装
$ yarn add prisma --dev

一旦安装,您可以通过yarn来执行命令

$ yarn prisma

那么现在创建您的初期Prisma设定使用init命令

$ npx prisma init

这个命令会创建新的prisma目录包含下面的内容:

  • shcema.prisma:指定您的数据连接并且包含数据库结构
  • .env:dotenv文件,主要是作为环境变量存储您的数据验证信息。

设定数据库连接

您的数据库连接被配置在了schema.prisma文件中的datasource块中。默认使用postgresql,但是因为要在这里使用SQLite数据库,所以需要修改datasourceprovider项目为sqlite

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

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

现在,打开.env并且修改DATABASE_URL环境变量如下:

DATABASE_URL="file:./dev.db"

确认您的配置了ConfigModule,否则这个DATABASE_URL变量将不会从.env文件中取出。

SQLite数据库是一些简单的文件;使用SQLite数据库不需要任何服务器.因此代替连接地址URL的host和port,您只需要在dev.db中指定本地的文件。这个文件将会在下面的步骤中被创建。

扩展:如果您使用PostgreSQL或者MySQL 使用PostgreSQL或者MySQL,您需要配置URL来指定数据库服务。您可以在这里学习一些必要的连接URL样式。

PostgreSQL

如果您使用PostgreSQL,您必须像下面这样修改schema.prisma.env文件:

schema.prisma

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

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

.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"
  url      = env("DATABASE_URL")
}

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

.env

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

将所有大写字母拼写的占位符替换为数据库凭据。

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

在这部分,您将会使用Prisma Migrate在数据库中创建2张表.Prisma Migrate为Prisma模式中的声明性数据模型定义生成SQL迁移文件。这些迁移文件是完全可自定义的,因此您可以配置底层数据库的任何附加功能,或包括附加命令,例如用于种子设定。

将下面2个模块添加到您的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?
}

通过运行下面的命令,您能够针对您的数据库产生对应的迁移文件。

$ npx prisma migrate dev --name init

prisma migrate dev命令产生SQL文件并直接在数据库中运行。在这个例子中,下面的迁移文件将会在既存目录 pirsma中被创建。

$ 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

注意在安装期间,Prisma会自动为您安装prisma generate命令。在将来,为了更新您的Prisma Client设定,每当您改变您的Prisma模型您需要运行这个命令。

注意 prisma generate命令读取您的prisma模式,并更新node_modules/@prisma/Client内生成的prisma-Client库。

在您的服务中使用Prisma Client

您现在能够通过Prisma Client来发送数据库请求语句。如果您想要学习更多关于用Prisma Client怎么构建语句,请查看API documenttation

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

src目录中,创建名为prisma.service.ts的文件并为它添加以下代码

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

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

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }
}

注意

onModuleInit是可选的--如果不考虑它,Prisma将在第一次调用数据库时延迟连接。我们不关心onModuleDestroy,因为Prisma有自己的关闭hook,它会回收连接。有关enableShutdownHooks的更多信息,请参阅enableShutdownHooks的问题

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

仍然是在src目录下,创建一个名为user.service.ts文件并添加以下内容:

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

@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客户端生成的类型来确保服务公开的方法类型正确。因此,您可以保存键入模型和创建额外接口或DTO文件的样板文件。

现在,Post也同样这样操作

也是在src目录下,创建一个名为post.service.ts文件并添加以下内容:

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

@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正确的包装了在PrismaClient可以获得的CRUD语句。在真实世界程序中,这个服务也会包含一些业务逻辑。比如:您可能会在UserService中有updatePassword方法,它对与某个用户的更新密码有责任。

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

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

用下面的代码来替换app.controller.ts中的内容。

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

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

这个控制器实现以下内容:

  • GET

    • /post/:id:通过id取的单个帖子
    • /feed:取得所有发布的帖子
    • /filter-posts/:searchString:通过title或者content来筛选帖子
  • POST

    • /post:创建一个新的post
      • Body:
        • title:String(必须):帖子中的标题
        • content:String(必须):帖子中的内容
        • authorEmail:String(必须):创建帖子的用户的邮箱
    • /user: 创建一个新的用户
      • Body:
        • email: String(必须):用户的邮箱地址
        • name:String(必须):用户的名字
  • PUT

    • /publish/:id:通过id来发布帖子
  • DELETE

    • /post/:id:通过id来删除一个帖子

enableShutdownHooks的问题

Prisma干扰NestJS enableShutdownHooks。Prisma 监听关闭信号 和 在应用程序关闭钩子被触发前会调用process.exit。为了解决这个问题,您需要在beforeExit事件中为了Primisa添加一个监听器。

// main.ts
...
import { PrismaService } from './services/prisma/prisma.service';
...
async function bootstrap() {
  ...
  const prismaService = app.get(PrismaService);
  await prismaService.enableShutdownHooks(app)
  ...
}
bootstrap()

您可以阅读更多关于Prisma处理关闭信号和beforeExit的信息。

总结

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

如果您想了解更多关于将NestJS与Prisma一起使用的信息,请务必查看以下资源: