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,并通过以下命令创建项目骨架:
$ 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 安装为开发依赖:
$ 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:指定数据库连接并包含数据库 schemaprisma.config.ts:项目配置文件.env:一个 dotenv 文件,通常用于存放数据库凭证等环境变量
设置生成器输出路径
你可以在执行 prisma init 时通过传入 --output ../src/generated/prisma 来指定生成的 Prisma Client 输出 path,也可以直接在 Prisma schema 中这样写:
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 module 形式发布,而这与 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 不需要数据库服务器。因此,与其配置带有 host 和 port 的连接 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 schema 中声明式的数据模型生成 SQL migration 文件。这些 migration 文件是完全可定制的,因此你可以针对底层数据库加入额外特性或自定义命令,例如数据种子逻辑。
将下面两个模型加入 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?
}模型定义好后,就可以生成 SQL migration 文件并将其应用到数据库中。请在终端执行:
$ npx prisma migrate dev --name init这个 prisma migrate dev 命令会生成 SQL 文件,并直接把它们运行到数据库中。本例中,它会在现有的 prisma 目录里创建如下 migration 文件:
$ 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 命令来生成项目所需的类型与 Client。只要 schema 发生变化,你都需要重新执行 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 的数据库查询能力封装进一个 service 中。首先可以创建一个新的 PrismaService,负责实例化 PrismaClient 并连接数据库。
在 src 目录中创建一个名为 prisma.service.ts 的新文件,并加入如下代码:
// 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 文件,并加入如下代码:
// 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 文件,并加入如下代码:
// 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 文件内容替换为:
// 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 的用户邮箱
- Body:
/user:创建一个新用户- Body:
email: String(必填):用户邮箱地址name: String(可选):用户名
- Body:
PUT
/publish/:id:根据id发布一个 post
DELETE
/post/:id:根据id删除一个 post
总结
在这篇秘籍中,你学习了如何将 Prisma 与 NestJS 结合使用来实现一个 REST API。负责 API 路由的 controller 会调用 PrismaService,后者再利用 Prisma Client 向数据库发送查询,从而满足传入请求的数据需求。
如果你想进一步了解如何在 NestJS 中使用 Prisma,建议继续查看以下资源: