
如何快速實現REST API集成以優化業務流程
cd real-world-grading-app
npm install
注意:通過查看
part-1
分支,您將能夠從相同的起點跟蹤文章。
要啟動PostgreSQL,請從real-world-grading-app
文件夾運行以下命令:
docker-compose up -d
注意:Docker 將使用
docker-compose.yml
文件啟動 PostgreSQL 容器。
在著手構建后端系統時,準確理解問題域是至關重要的第一步。問題域,亦稱問題空間,涵蓋了定義問題及其解決方案約束的全部信息。通過深入剖析問題域,我們能夠清晰地勾勒出數據模型的輪廓和結構。
針對在線評分系統,我們確定了以下核心實體:
(注:實體既可以表示物理對象,也可以是無形的概念。例如,用戶對應的是真實的人,而課程則是一個抽象的概念。)
為了更直觀地展示這些實體及其在關系數據庫(本例中為PostgreSQL)中的表示,我們可以構建一幅圖表。該圖表不僅列出了每個實體及其外鍵,還詳細描述了實體間的關聯關系及對應的列。
關于該圖,首先要注意的是,每個實體都映射到一個數據庫表。
該圖具有以下關系:
1-N
):
M-N
):
注意:關系表(也稱為JOIN表)用于連接兩個或多個其他表,從而在它們之間建立關系。在SQL中,創建關系表是表示不同實體之間關系的常見數據建模實踐。本質上,這意味著“在數據庫中,一個m-n關系被建模為兩個1-n關系”。
要在數據庫中創建表,首先需要定義Prisma架構。Prisma架構是數據庫表的聲明性配置,Prisma Migrate將使用此配置在數據庫中創建表。與上面的實體圖類似,它定義了數據庫表之間的列和關系。
(Prisma架構是生成的Prisma客戶端和Prisma Migrate創建數據庫架構的真實來源。)
項目的Prisma架構可以在prisma/schema.prisma中找到。在架構中,您將找到在此步驟中定義的模型(對應數據庫表)的占位符和一個datasource塊。datasource塊定義了要連接的數據庫類型以及連接字符串。通過使用env(“DATABASE_URL”),Prisma將從環境變量中加載數據庫連接URL。
注意:將秘密隱藏在代碼庫之外被認為是最佳實踐。因此,在datasource塊中定義了env(“DATABASE_URL”)。通過設置環境變量,您可以將秘密隱藏在代碼庫之外。
Prisma架構的核心組成部分是model。每個model都直接對應數據庫中的一個表。
以下是一個示例,展示了model的基本結構:
model User {
id Int @default(autoincrement()) @id
email String @unique
firstName String
lastName String
social Json?
}
在這里,您定義了一個帶有多個字段的User
模型。每個字段都有一個名稱,后跟一個類型和可選的字段屬性。例如,id
字段可以分解如下:
名字 | 類型 | 標量與關系 | 類型修飾符 | 屬性 |
---|---|---|---|---|
id | Int | 標量 | – | @id (表示主鍵)和 @default(autoincrement()) (設置默認自增值) |
email | String | 標量 | – | @unique |
firstName | String | 標量 | – | – |
lastName | String | 標量 | – | – |
social | Json | 標量 | ? (自選) | – |
Prisma 提供了一系列數據類型,這些數據類型會根據您所使用的數據庫自動映射到相應的本機數據庫類型。
其中,Json 數據類型允許您存儲自由格式的 JSON 數據。這對于在 User 記錄中存儲可能不一致或頻繁變更的信息特別有用,因為這些變更可以在不影響后端核心功能的情況下輕松進行。在上面的 User 模型中,Json 數據類型被用于存儲如 Twitter、LinkedIn 等社交鏈接。當您需要向 social 字段添加新的社交配置文件鏈接時,無需進行數據庫遷移。
在充分理解問題域并掌握了使用 Prisma 進行數據建模的方法后,您現在可以將以下模型添加到 prisma/schema.prisma
文件中:
model User {
id Int @default(autoincrement()) @id
email String @unique
firstName String
lastName String
social Json?
}
model Course {
id Int @default(autoincrement()) @id
name String
courseDetails String?
}
model Test {
id Int @default(autoincrement()) @id
updatedAt DateTime @updatedAt
name String // Name of the test
date DateTime // Date of the test
}
model TestResult {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
result Int // Percentage precise to one decimal point represented as result * 10^-1
}
每個模型都有所有相關的字段,而忽略關系(將在下一步中定義)。
在此步驟中,您將在 Test
和 TestResult
之間定義一個一對多關系。
首先,考慮在上一步中定義的Test
和TestResult
模型:
model Test {
id Int @default(autoincrement()) @id
updatedAt DateTime @updatedAt
name String
date DateTime
}
model TestResult {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
result Int // Percentage precise to one decimal point represented result * 10^-1
}
要定義兩個模型之間的一對多關系,請添加以下三個字段:
model Test {
id Int @default(autoincrement()) @id
updatedAt DateTime @updatedAt
name String
date DateTime
testResults TestResult[]// relation field
}
model TestResult {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
result Int // Percentage precise to one decimal point represented result * 10^-1
testId Int // relation scalar field
test Test @relation(fields: [testId], references: [id]) // relation field
}
關系字段,如test和testResults,可以通過其值類型指向另一個模型(例如Test和TestResult)來識別。這些字段的名稱將影響使用Prisma客戶端以編程方式訪問關系的方式,但它們并不代表真實的數據庫列。
在此步驟中,您將在 User
和 Course
模型之間定義一個多對多關系。
多對多關系在 Prisma 架構中可以是隱式的,也可以是顯式的。在這一部分中,您將了解兩者之間的區別以及何時選擇隱式或顯式。
首先,考慮在上一步中定義的 Test
和 TestResult
模型:
model User {
id Int @default(autoincrement()) @id
email String @unique
firstName String
lastName String
social Json?
}
model Course {
id Int @default(autoincrement()) @id
name String
courseDetails String?
}
要創建隱式多對多關系,請將關系字段定義為關系兩側的列表:
model User {
id Int @default(autoincrement()) @id
email String @unique
firstName String
lastName String
social Json?
courses Course[]
}
model Course {
id Int @default(autoincrement()) @id
name String
courseDetails String?
members User[]
}
這樣,Prisma 將根據所定義的屬性來構建關系表,從而支持分級系統:
然而,評分系統的一個關鍵需求是允許用戶以教師或學生的身份與課程相關聯。這意味著我們需要一種機制來存儲關于數據庫中關系的“附加信息”。
為了滿足這一需求,我們可以采用顯式的多對多關系。具體來說,我們需要為連接User和Course的關系表添加一個名為CourseEnrollment的新模型,并在該關系表中添加額外的字段來指明用戶是課程的教師還是學生。使用顯式的多對多關系允許我們在關系表上定義這些額外的字段。
為此,我們將對User和Course模型進行更新,將它們的courses和members字段的類型更改為CourseEnrollment[],如下所示:
model User {
id Int @default(autoincrement()) @id
email String @unique
firstName String
lastName String
social Json?
courses CourseEnrollment[]
}
model Course {
id Int @default(autoincrement()) @id
name String
courseDetails String?
members CourseEnrollment[]
}
model CourseEnrollment {
createdAt DateTime @default(now())
role UserRole
// Relation Fields
userId Int
user User @relation(fields: [userId], references: [id])
courseId Int
course Course @relation(fields: [courseId], references: [id])
@@id([userId, courseId])
@@index([userId, role])
}
enum UserRole {
STUDENT
TEACHER
}
關于CourseEnrollment模型的幾點說明:
@@id([userId, courseId])
定義了兩個字段的多字段主鍵。這確保了每個用戶只能以特定角色(學生或教師)與每個課程關聯一次,但不能同時擁有兩種角色。@@index([userId, role])
為userId和role字段創建了索引,這有助于提高基于這些字段進行查詢的效率。現在您已經了解了關系的定義方式,請使用以下內容更新 Prisma 架構:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
firstName String
lastName String
social Json?
// Relation fields
courses CourseEnrollment[]
testResults TestResult[] @relation(name: "results")
testsGraded TestResult[] @relation(name: "graded")
}
model Course {
id Int @id @default(autoincrement())
name String
courseDetails String?
// Relation fields
members CourseEnrollment[]
tests Test[]
}
model CourseEnrollment {
createdAt DateTime @default(now())
role UserRole
// Relation Fields
userId Int
courseId Int
user User @relation(fields: [userId], references: [id])
course Course @relation(fields: [courseId], references: [id])
@@id([userId, courseId])
@@index([userId, role])
}
model Test {
id Int @id @default(autoincrement())
updatedAt DateTime @updatedAt
name String
date DateTime
// Relation Fields
courseId Int
course Course @relation(fields: [courseId], references: [id])
testResults TestResult[]
}
model TestResult {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
result Int // Percentage precise to one decimal point represented as result * 10^-1
// Relation Fields
studentId Int
student User @relation(name: "results", fields: [studentId], references: [id])
graderId Int
gradedBy User @relation(name: "graded", fields: [graderId], references: [id])
testId Int
test Test @relation(fields: [testId], references: [id])
}
enum UserRole {
STUDENT
TEACHER
}
請注意,TestResult模型與User模型之間存在兩個關系:student和gradedBy。student關系代表參加考試的學生,而gradedBy關系則代表給考試評分的老師。當一個模型與另一個模型之間存在多個關系時,我們需要使用@relation參數在關系的name屬性上指定,以消除這些關系之間的歧義。
在定義了Prisma模式之后,接下來您將使用Prisma Migrate在數據庫中創建實際的表結構。
首先,在本地設置 DATABASE_URL
環境變量,以便Prisma可以連接到您的數據庫。
export DATABASE_URL="postgresql://prisma:prisma@127.0.0.1:5432/grading-app"
注意:本地數據庫的用戶名和密碼都定義為
prisma
中的docker-compose.yml
。
要使用 Prisma Migrate 創建并運行遷移,請在終端中運行以下命令:
npx prisma migrate dev --preview-feature --skip-generate --name "init"
該命令將執行兩項操作:
prisma/migrations
注意:Prisma Migrate 當前處于預覽模式。這意味著不建議在生產中使用 Prisma Migrate。
檢查點:您應該在輸出中看到如下內容:
Prisma Migrate created and applied the following migration(s) from new schema changes:
migrations/
└─ 20201202091734_init/
└─ migration.sql
Everything is now in sync.
恭喜,您已成功設計數據模型并創建數據庫架構。在下一步中,您將使用 Prisma Client 對數據庫執行 CRUD 和聚合查詢。
Prisma Client 是為您的數據庫架構量身定制的自動生成的數據庫客戶端。它的工作原理是解析 Prisma 架構并生成一個 TypeScript 客戶端,您可以將其導入到代碼中。
生成 Prisma 客戶端通常需要三個步驟:
generator
定義添加到您的Prisma模式: generator client { provider = "prisma-client-js" }
@prisma/client
npm包 :npm install --save @prisma/client
npx prisma generate
檢查點:您應該在輸出中看到以下內容:? Generated Prisma Client to ./node_modules/@prisma/client in 57ms
在此步驟中,您將使用 Prisma Client 編寫種子腳本,目的是用一些示例數據來填充您的數據庫。
在這個上下文中,種子腳本其實就是一組利用 Prisma Client 執行的 CRUD(創建、讀取、更新和刪除)操作。此外,您還可以利用嵌套寫入的功能,在單個操作中為相關的數據庫實體創建記錄。
打開框架src/seed.ts
文件,您將在其中找到導入的Prisma Client和兩個Prisma Client函數調用:一個用于實例化Prisma Client,另一個用于在腳本完成運行時斷開連接。
開始,在 main
函數中創建一個用戶,如下所示:
const grace = await prisma.user.create({
data: {
email: 'grace@hey.com',
firstName: 'Grace',
lastName: 'Bell',
social: {
facebook: 'gracebell',
twitter: 'therealgracebell',
},
},
})
該操作將在User表中創建一行,并返回創建的用戶(包括創建的id
)。值得注意的是,user
將推斷出在User
中定義的類型@prisma/client
:
export type User = {
id: number
email: string
firstName: string
lastName: string
social: JsonValue | null
}
要執行seed腳本并創建 User
記錄,可以在 seed
中使用 package.json
腳本,如下所示:
npm run seed
在執行接下來的步驟時,您可能需要多次運行種子腳本。為了防止因重復運行而遇到唯一性約束錯誤(例如主鍵沖突或唯一索引沖突),您可以在?main
?函數的開頭添加代碼來清空數據庫的內容。
await prisma.testResult.deleteMany({})
await prisma.courseEnrollment.deleteMany({})
await prisma.test.deleteMany({})
await prisma.user.deleteMany({})
await prisma.course.deleteMany({})
注意:這些命令將刪除每個數據庫表中的所有行。請謹慎使用,并在生產中避免這種情況!
在此步驟中,您將創建一個課程并使用嵌套寫入創建相關測試。
將以下內容添加到 main
函數:
const weekFromNow = add(new Date(), { days: 7 })
const twoWeekFromNow = add(new Date(), { days: 14 })
const monthFromNow = add(new Date(), { days: 28 })
const course = await prisma.course.create({
data: {
name: 'CRUD with Prisma',
tests: {
create: [
{
date: weekFromNow,
name: 'First test',
},
{
date: twoWeekFromNow,
name: 'Second test',
},
{
date: monthFromNow,
name: 'Final exam',
},
],
},
},
})
這將會在 Course
表中插入一行數據,并且在 Tests
表中插入三個與之相關聯的行(由于 Course
和 Tests
之間存在一對多的關系,因此可以這樣做)。
如果您希望將上一步中創建的用戶與本課程的教師角色建立關聯,那么應該怎么做呢?
User
和 Course
之間存在一個顯式的多對多關系。這意味著我們需要在 CourseEnrollment
表中插入一行數據,并通過分配一個特定的角色來將 User
與 Course
關聯起來。
這可以按如下方式完成(添加到上一步的查詢中):
const weekFromNow = add(new Date(), { days: 7 })
const twoWeekFromNow = add(new Date(), { days: 14 })
const monthFromNow = add(new Date(), { days: 28 })
const course = await prisma.course.create({
data: {
name: 'CRUD with Prisma',
tests: {
create: [
{
date: weekFromNow,
name: 'First test',
},
{
date: twoWeekFromNow,
name: 'Second test',
},
{
date: monthFromNow,
name: 'Final exam',
},
],
},
members: {
create: {
role: 'TEACHER',
user: {
connect: {
email: grace.email,
},
},
},
},
},
include: {
tests: true,
},
})
注意:
include
參數允許你獲取結果中的關系。這將有助于在后面的步驟中將測試結果與測試相關聯
當采用嵌套寫入(例如為 members
和 tests
)時,您有兩個選項可供選擇:
在 tests
的例子中,你傳遞了一個對象數組,這些對象與剛剛創建的課程存在關聯。
至于 members
的情況,create
和 connect
兩者都會被用到。這是至關重要的,因為即便用戶數據已經存在于數據庫中,也仍然需要在關系表(即 CourseEnrollment
,它引用了 members
)中插入一行新的數據。這行新數據會利用 connect
與之前已創建的用戶數據建立起關系。
在上一步中,您創建了課程、相關測試,并為該課程分配了一名教師。在此步驟中,您將創建更多用戶,并將他們作為學生與課程關聯。
添加以下語句:
const shakuntala = await prisma.user.create({
data: {
email: 'devi@prisma.io',
firstName: 'Shakuntala',
lastName: 'Devi',
courses: {
create: {
role: 'STUDENT',
course: {
connect: { id: course.id },
},
},
},
},
})
const david = await prisma.user.create({
data: {
email: 'david@prisma.io',
firstName: 'David',
lastName: 'Deutsch',
courses: {
create: {
role: 'STUDENT',
course: {
connect: { id: course.id },
},
},
},
},
})
看看TestResult
模型,它有三個關系:student
、gradedBy
和test
。要為Shakuntala和大衛添加測試結果,您將使用與前面步驟類似的嵌套寫入。
以下是TestResult
模型再次供參考:
model TestResult {
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
result Int // Percentage precise to one decimal point represented as result * 10^-1
// Relation Fields
studentId Int
student User @relation(name: "results", fields: [studentId], references: [id])
graderId Int
gradedBy User @relation(name: "graded", fields: [graderId], references: [id])
testId Int
test Test @relation(fields: [testId], references: [id])
}
添加單個測試結果將如下所示:
await prisma.testResult.create({
data: {
gradedBy: {
connect: { email: grace.email },
},
student: {
connect: { email: shakuntala.email },
},
test: {
connect: { id: test.id },
},
result: 950,
},
})
要為這三個測試中的每一個添加 David 和 Shakuntala 的測試結果,您可以創建一個循環:
const testResultsDavid = [650, 900, 950]
const testResultsShakuntala = [800, 950, 910]
let counter = 0
for (const test of course.tests) {
await prisma.testResult.create({
data: {
gradedBy: {
connect: { email: grace.email },
},
student: {
connect: { email: shakuntala.email },
},
test: {
connect: { id: test.id },
},
result: testResultsShakuntala[counter],
},
})
await prisma.testResult.create({
data: {
gradedBy: {
connect: { email: grace.email },
},
student: {
connect: { email: david.email },
},
test: {
connect: { id: test.id },
},
result: testResultsDavid[counter],
},
})
counter++
}
恭喜您!如果您已經順利完成了上述步驟,那么就意味著您已經在數據庫中成功創建了用戶、課程、測試以及測試結果的示例數據。
為了直觀地查看并瀏覽這些數據,您可以運行 Prisma Studio。Prisma Studio 是一個功能強大的數據庫可視化工具。要啟動 Prisma Studio,您只需在終端中輸入并執行以下命令:
npx prisma studio
Prisma Client 允許您對模型的數字字段(例如 和 )執行聚合操作。聚合操作從一組 Importing 值(即表中的多行)計算單個結果。例如,計算一組行中列的最小值、最大值和平均值。Int
Float
result
TestResult
在此步驟中,您將運行兩種聚合操作:
1.對于所有學生的課程中的每個測試,生成表示測試難度或班級對測試主題的理解的聚合:
for (const test of course.tests) {
const results = await prisma.testResult.aggregate({
where: {
testId: test.id,
},
avg: { result: true },
max: { result: true },
min: { result: true },
count: true,
})
console.log(test: ${test.name} (id: ${test.id})
, results)
}
這將導致以下結果:
test: First test (id: 1) {
avg: { result: 725 },
max: { result: 800 },
min: { result: 650 },
count: 2
}
test: Second test (id: 2) {
avg: { result: 925 },
max: { result: 950 },
min: { result: 900 },
count: 2
}
test: Final exam (id: 3) {
avg: { result: 930 },
max: { result: 950 },
min: { result: 910 },
count: 2
}
2.對于所有測試中的每個學生,生成表示學生在課程中的表現的聚合:
// Get aggregates for David
const davidAggregates = await prisma.testResult.aggregate({
where: {
student: { email: david.email },
},
avg: { result: true },
max: { result: true },
min: { result: true },
count: true,
})
console.log(David's results (email: ${david.email})
, davidAggregates)
// Get aggregates for Shakuntala
const shakuntalaAggregates = await prisma.testResult.aggregate({
where: {
student: { email: shakuntala.email },
},
avg: { result: true },
max: { result: true },
min: { result: true },
count: true,
})
console.log(Shakuntala's results (email: ${shakuntala.email})
, shakuntalaAggregates)
這將導致以下終端輸出:
David's results (email: david@prisma.io) {
avg: { result: 833.3333333333334 },
max: { result: 950 },
min: { result: 650 },
count: 3
}
Shakuntala's results (email: devi@prisma.io) {
avg: { result: 886.6666666666666 },
max: { result: 950 },
min: { result: 800 },
count: 3
}
本文廣泛涵蓋了多個基礎領域,從問題域的初步探索,到數據建模的深入研究,再到Prisma Schema的解析、使用Prisma Migrate進行數據庫遷移的方法,以及借助Prisma Client執行CRUD操作和聚合的技巧。
在著手編寫代碼之前,明智之舉是先規劃出問題域。因為問題域將指導數據模型的設計思路,進而對后端的各個環節產生深遠影響。
盡管Prisma致力于簡化關系數據庫的使用流程,但深入理解底層數據庫的運作機制仍大有裨益。
建議您查閱Prisma的數據指南,以獲取關于數據庫工作原理的詳盡信息、數據庫選擇的策略,以及如何將數據庫與應用程序高效融合,從而充分發揮其效能。
在本文系列的后續篇章中,我們將深入介紹以下主題:
原文鏈接:https://www.prisma.io/blog/backend-prisma-typescript-orm-with-postgresql-data-modeling-tsjs1ps7kip1