Prisma是一个开源Node.js的ORM且使用TypeScript。它被用作编写普通SQL或使用其他数据库访问工具的替代方案,如SQL查询生成器(如knex.js)或ORM(如TypeORM和Sequelize),Prisma现在支持SQL, MySQL, SQL Server, SQLite, MongoDB 和 CockroachDB (预览)。
虽然Prisma可以与普通JavaScript一起使用,但它包含了TypeScript,并提供了一个超出TypeScript生态系统中其他ORM保证的类型安全级别。您可以在这里找到Prisma和TypeORM的类型安全保证的深入比较。
- 注意
如果您想要快速的知道怎么让Prisma工作,你可以查看Quickstart或者阅读文档里面的介绍.这里也有一些已经做好的REST和GraphQL例子放在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中实现。等读完这篇指导,您将会添加新的路由它实现了关于users和posts的数据存取。
设定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数据库,所以需要修改datasource中provider项目为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,
});
}
}
您的UserService和PostService正确的包装了在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(必须):创建帖子的用户的邮箱
- Body:
/user: 创建一个新的用户- Body:
email: String(必须):用户的邮箱地址name:String(必须):用户的名字
- Body:
-
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一起使用的信息,请务必查看以下资源: