iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0
Modern Web

一些讓你看來很強的 ORM - prisma系列 第 15

Day15. 一些讓你看來很強的 ORM - prisma (Self Relation)

  • 分享至 

  • xImage
  •  

今天的要來介紹 Self Relations ,第一次聽到這名詞的你可能會覺得很酷, relation 除了一對一、一對多、多對多外,自己原來也可以 relation 自己,會有 Self Relations 的出現原因是,我們的業務需求中,對於 model 的使用上,會有同一個 model 但是不同解讀的方式,舉個例子以 teacherstudent 來說,他們的 model 都是屬於 User ,同時學生可能會有他的老師,相反老師可能也會有自己的學生,所以這個情形就很適合用 Self Relations 去描敘關聯的關係,Self Relations 的結構上,很像是一個上下層的概念去 chain 在一起, 很特別的是他可以用在 1-11-nm-n 中,這邊我們將慢慢介紹~

One-To-One Self-Relations

這邊就是一個單純的一對一的關係,你會看到:

  • 一個 user 可能會有 0 個或一個前輩
  • 一個 user 可能會有 0 個或一個繼任者

這邊要注意的是,如果是 1-1Self-Relations 兩邊的 relation 不能都是 required 因為這樣你會無法 create 第一筆 User1-1 必需要先有 user list 後,你才能用類似於 linklist 的方式,去指定該 User 的上下關係,並 connect 這位 User 的前輩是誰繼任者是誰

model User {
  id          Int     @id @default(autoincrement())
  name        String?
  successorId Int?    @unique
  successor   User?   @relation("BlogOwnerHistory", fields: [successorId], references: [id])
  predecessor User?   @relation("BlogOwnerHistory")
}

這邊有幾個小規則:

  • 兩邊的 relation 必須要有相同的 name 例如這邊的 BlogOwnerHistory
  • 其中一邊的 relation 要完整描述,relation 的全部內容
  • 1-1 的寫法一樣,其中一邊要當作 foreign key 去關聯到哪個 User 這邊來說就是 successorId

同時被定義成 foreign key 的欄位,這邊來說就是 successorId ,必須要是 unique 的以確保 1-1 的唯一性

model User {
  id          Int     @id @default(autoincrement())
  name        String?
  successorId Int?    @unique
  successor   User?   @relation("BlogOwnerHistory", fields: [successorId], references: [id])
  predecessor User?   @relation("BlogOwnerHistory")
}

那因為是 1-1 所以你要反過來讓 predecessorId 當作 foreign key 也是 OK 一樣你需要確保 predecessorIdunique

model User {
  id            Int     @id @default(autoincrement())
  name          String?
  successor     User?   @relation("BlogOwnerHistory")
  predecessorId Int?    @unique
  predecessor   User?   @relation("BlogOwnerHistory", fields: [predecessorId], references: [id])
}

那不管你是 predecessorId 或是 successorId 當你的 foreign key ,在 prisma client 寫法都是一樣的~

const x = await prisma.user.create({
  data: {
    name: "Bob McBob",
    successor: {
      connect: {
        id: 2,
      },
    },
    predecessor: {
      connect: {
        id: 4,
      },
    },
  },
});

以下是 SQL 對於 1-1 Self Relation 的語法,跟 1-1SQL 一樣的是需要對 successorId 加上 UniqueCONSTRAINT

CREATE TABLE "User" (
    id SERIAL PRIMARY KEY,
    "name" TEXT,
    "successorId" INTEGER
);

ALTER TABLE "User" ADD CONSTRAINT fk_successor_user FOREIGN KEY ("successorId") REFERENCES "User" (id);

ALTER TABLE "User" ADD CONSTRAINT successor_unique UNIQUE ("successorId");

One-To-Many Self-Relations

這邊的 1-n 的關係會是

  • 一個 student 可能會有 0 個或一個 teacher
  • 一個 teacher 可能會有 0 個或多個 student
model User {
  id        Int     @id @default(autoincrement())
  name      String?
  teacherId Int?
  teacher   User?   @relation("TeacherStudents", fields: [teacherId], references: [id])
  students  User[]  @relation("TeacherStudents")
}

那跟 1-nSQL 一樣,不需要在 teacherId 加上 unique ,因為多個學生,可能會給同一個 teacher 教到~

CREATE TABLE "User" (
    id SERIAL PRIMARY KEY,
    "name" TEXT,
    "teacherId" INTEGER
);

ALTER TABLE "User" ADD CONSTRAINT fk_teacherid_user FOREIGN KEY ("teacherId") REFERENCES "User" (id);

Many-To-Many Self-Relations

這邊的 m-n 的關係會是

  • 一個 user 可以被 followed 0 到多個 user
  • 一個 user 可能會去 follow 0 到多個 user

這邊跟 m-n 的寫法一樣都適用 implicitprisma 自動幫你管理中間表的部分

model User {
  id         Int     @id @default(autoincrement())
  name       String?
  followedBys User[]  @relation("UserFollows")
  followings  User[]  @relation("UserFollows")
}

那如果需要管理中間表的話可以用 explicit mode

model User {
  id         Int     @id @default(autoincrement())
  name       String?
  followedBys Follows[]  @relation("followedBy")
  followings  Follows[]  @relation("following")
}

model Follows {
  followedBy   User @relation("followedBy", fields: [followedById], references: [id])
  followedById Int
  following    User @relation("following", fields: [followingId], references: [id])
  followingId  Int

  @@id([followingId, followedById])
}

對應的 SQL 語法如下

CREATE TABLE "User" (
    id integer DEFAULT nextval('"User_id_seq"'::regclass) PRIMARY KEY,
    name text
);
CREATE TABLE "_UserFollows" (
    "A" integer NOT NULL REFERENCES "User"(id) ON DELETE CASCADE ON UPDATE CASCADE,
    "B" integer NOT NULL REFERENCES "User"(id) ON DELETE CASCADE ON UPDATE CASCADE
);

prisma client 的寫法如下~你會發現不管是 implicit 或是 explicit 都跟之前的寫法一樣,只是變成兩個欄位 followedBysfollowingsconnect

//implicit
const newUser = await prismaClient.user.create({
    data: {
      name: "Danny3.0",
      followedBys: {
        connect: {
          id: 1
        }
      },
      followings: {
        connect: [
          { id: 5 }, { id: 6 }, { id: 7 }
        ]
      }
    }
  })
//explicit
const newUser = await prismaClient.user.create({
    data: {
      name: "Danny3.0",
      followedBys: {
        create: [{
          followedBy: {
            connect: {
              id: 1
            }
          }
        }]
      },
      followings: {
        createMany: {
          data: [
            { followingId: 5 },
            { followingId: 6 },
            { followingId: 7 },
          ]
        }
      }
    }
  })

補充

Self Relation 很特別的是可以同時寫 1-11-nm-n 情況在同一個 model

model User {
  id         Int     @id @default(autoincrement())
  name       String?
  teacherId  Int?
  teacher    User?   @relation("TeacherStudents", fields: [teacherId], references: [id])
  students   User[]  @relation("TeacherStudents")
  followedBy User[]  @relation("UserFollows")
  following  User[]  @relation("UserFollows")
}

大家如果有問題可以來小弟的群組討論~

✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day14. 一些讓你看來很強的 ORM - prisma (Fluent API)
下一篇
Day16. 一些讓你看來很強的 ORM - prisma (Validator)上
系列文
一些讓你看來很強的 ORM - prisma30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言