跳至主要內容

如何在 NestJS 中使用 Prisma ORM

20 分鐘

簡介

本指南將展示如何將 Prisma ORM 與 NestJS 搭配使用。NestJS 是一個用於構建高效且可擴展的伺服器端應用程式的漸進式 Node.js 框架。您將使用 NestJS 構建一個 REST API,並利用 Prisma ORM 來存儲與檢索資料庫中的資料。

Prisma ORM 是一個適用於 Node.js 與 TypeScript 的開源 ORM。它作為編寫原始 SQL,或使用其他資料庫存取工具(如 SQL 查詢構建器,例如 knex.js)或 ORM(如 TypeORMSequelize)的替代方案。Prisma 目前支援 PostgreSQL、MySQL、SQL Server、SQLite、MongoDB 與 CockroachDB。

雖然 Prisma 可以與原始 JavaScript 一起使用,但它深度整合了 TypeScript,並提供了超越 TypeScript 生態系統中其他 ORM 所能保證的型別安全級別。

您可以在此處找到一個可直接執行的範例。

先決條件

1. 建立您的 NestJS 專案

安裝 NestJS CLI 並建立一個新專案

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

出現提示時,請選擇 npm 作為您的套件管理工具。接著導航至專案目錄

cd nestjs-prisma

您可以執行 npm start 以在 https://:3000/ 啟動您的應用程式。在本指南的過程中,您將新增路由來儲存和檢索有關 使用者 (users)文章 (posts) 的資料。

2. 設定 Prisma

2.1. 安裝 Prisma 與相依套件

安裝必要的 Prisma 套件與資料庫驅動程式

npm install prisma --save-dev
npm install @prisma/client @prisma/adapter-pg pg
資訊

如果您使用的是其他資料庫提供者(PostgreSQL、MySQL、SQL Server),請安裝對應的驅動程式適配器套件,而非 @prisma/adapter-pg。如需更多資訊,請參閱 資料庫驅動程式 (Database drivers)

2.2. 初始化 Prisma

在您的專案中初始化 Prisma

npx prisma init --db --output ../src/generated/prisma

這會建立一個包含以下內容的 prisma 目錄

  • schema.prisma:指定您的資料庫連線並包含資料庫架構 (schema)
  • prisma.config.ts:您專案的設定檔
  • .env:一個 dotenv 檔案,通常用於在一組環境變數中儲存您的資料庫憑證

2.3. 設定產生器輸出路徑

您可以透過在 prisma init 時傳遞 --output ../src/generated/prisma,或直接在您的 Prisma schema 中,指定產生之 Prisma 客戶端的輸出 path

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

2.4. 設定您的資料庫連線

您的資料庫連線是在 schema.prisma 檔案中的 datasource 區塊設定的。預設情況下它被設定為 postgresql,這正是本指南所需的設定。

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

datasource db {
provider = "postgresql"
}

現在,開啟 .env,您應該會看到已經指定了 DATABASE_URL

.env
DATABASE_URL=""
注意

請確保您已設定 ConfigModule,否則 DATABASE_URL 變數將無法從 .env 中讀取。

2.5. 定義您的資料模型

將以下兩個模型加入到您的 schema.prisma 檔案中

prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean? @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}

2.6. 建立並執行遷移 (Migration)

設定好 Prisma 模型後,您可以產生 SQL 遷移檔案並對資料庫執行它們。在您的終端機中執行以下指令

npx prisma migrate dev --name init

prisma migrate dev 指令會產生 SQL 檔案並直接對資料庫執行。在這種情況下,現有的 prisma 目錄中將建立下列遷移檔案

$ tree prisma
prisma
├── migrations
│ └── 20201207100915_init
│ └── migration.sql
└── schema.prisma

2.7. 產生 Prisma Client

安裝完成後,您可以執行產生指令來建立專案所需的型別與客戶端。如果您的 schema 有任何變更,您需要重新執行 generate 指令以保持這些型別同步。

npx prisma generate

3. 建立一個 Prisma 服務

現在您可以使用 Prisma Client 發送資料庫查詢了。在設定 NestJS 應用程式時,您會希望將 Prisma Client API 的資料庫查詢抽象化到一個服務中。首先,您可以建立一個新的 PrismaService,負責實例化 PrismaClient 並連線至您的資料庫。

src 目錄中,建立一個名為 prisma.service.ts 的新檔案,並加入以下程式碼

src/prisma.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaClient } from './generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

@Injectable()
export class PrismaService extends PrismaClient {
constructor() {
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL as string,
});
super({ adapter });
}
}

4. 建立 User 與 Post 服務

接下來,您可以編寫服務來針對 Prisma schema 中的 UserPost 模型進行資料庫呼叫。

4.1. 建立 User 服務

同樣在 src 目錄中,建立一個名為 user.service.ts 的新檔案,並加入以下程式碼

src/user.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { User, Prisma } from './generated/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 Client 產生的型別來確保服務公開的方法具有正確的型別定義。因此,您節省了為模型定義型別以及建立額外介面或 DTO 檔案的重複性工作 (boilerplate)。

4.2. 建立 Post 服務

現在對 Post 模型執行相同的操作。

同樣在 src 目錄中,建立一個名為 post.service.ts 的新檔案,並加入以下程式碼

src/post.service.ts

import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Post, Prisma } from './generated/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 目前封裝了 Prisma Client 中可用的 CRUD 查詢。在實際的應用程式中,服務也是新增商業邏輯的地方。例如,您可以在 UserService 中擁有一個名為 updatePassword 的方法,負責更新使用者的密碼。

5. 實作 REST API 路由

5.1. 建立控制器 (Controller)

最後,您將使用在前幾節中建立的服務來實作應用程式的不同路由。在本指南中,您將把所有路由放入現有的 AppController 類別中。

使用以下程式碼替換 app.controller.ts 檔案的內容

src/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 } from './generated/prisma/client';
import { Post as PostModel } from './generated/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 | null> {
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:獲取所有已發佈的文章
  • /filtered-posts/:searchString:依 titlecontent 篩選文章

POST

  • /post:建立新文章
    • 主體 (Body)
      • title: String (必填):文章標題
      • content: String (選填):文章內容
      • authorEmail: String (必填):建立文章的使用者電子郵件
  • /user:建立新使用者
    • 主體 (Body)
      • email: String (必填):使用者電子郵件地址
      • name: String (選填):使用者名稱

PUT

  • /publish/:id:依 id 發佈文章

DELETE

  • /post/:id:依 id 刪除文章

5.2. 在 App Module 中註冊服務

記得在 App Module 中註冊新服務。

更新 src/app.module.ts 以註冊所有服務

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ConfigModule } from '@nestjs/config';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { UserService } from './user.service';
import { PostService } from './post.service';

@Module({
imports: [ConfigModule.forRoot()],
controllers: [AppController],
providers: [AppService, PrismaService, UserService, PostService],
})
export class AppModule {}

6. 測試您的 API

啟動您的應用程式

npm start

使用 curl、PostmanHTTPie 測試您的端點。

建立使用者

curl -X POST https://:3000/user \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@prisma.io"}'

建立文章

curl -X POST https://:3000/post \
-H "Content-Type: application/json" \
-d '{"title": "Hello World", "authorEmail": "alice@prisma.io"}'

獲取已發佈文章

curl https://:3000/feed

發佈文章

curl -X PUT https://:3000/publish/1

搜尋文章

curl https://:3000/filtered-posts/hello

總結

在本指南中,您學習了如何將 Prisma ORM 與 NestJS 搭配使用來實作 REST API。實作 API 路由的控制器會呼叫 PrismaService,後者則利用 Prisma Client 發送查詢至資料庫,以滿足傳入請求的資料需求。

如果您想了解更多關於在 NestJS 中使用 Prisma 的資訊,請務必查看以下資源


與 Prisma 保持聯繫

透過以下方式與我們聯繫,繼續您的 Prisma 旅程: 我們的活躍社群。保持資訊靈通、參與其中,並與其他開發者合作

我們衷心感謝您的參與,並期待您成為我們社群的一份子!

© . This site is unofficial and not affiliated with Prisma Data, Inc.