iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
Modern Web

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

Day13. 一些讓你看來很強的 ORM - prisma (relation)m-n

  • 分享至 

  • xImage
  •  

今天的主題是關於 Many-to-many relations,他代表著關聯的 relation 他可以是 0 筆或是多筆資料的關聯,所以多對多的關聯的欄位他是 optional,那下面我們將更詳細介紹多對多的特點,廢話不多說我們走起~

介紹

在 relation database 中處理 m-n relation 也就是多對多的關聯是資料庫透過relation tables 去管理的,意思是透過另外一個 table 去管理兩個 table 的關聯內容,以下簡稱關係表,其中 prisma 提供 explicit 跟 implicit 兩種方式管理 schema

explicit

直接了當的把 relation 中管理多對多的情況所有的 schema 都寫出來,在關係表中 TagsOnPosts 多了 assignedAt 跟 assignedBy

model Post {
  id         Int                 @id @default(autoincrement())
  title      String
  categories TagsOnPosts[]
}

model Tag {
  id    Int                 @id @default(autoincrement())
  name  String
  posts TagsOnPosts[]
}

model TagsOnPosts {
  post       Post     @relation(fields: [postId], references: [id])
  postId     Int // relation scalar field (used in the `@relation` attribute above)
  tag   Category @relation(fields: [tagId], references: [id])
  tagId Int // relation scalar field (used in the `@relation` attribute above)
  assignedAt DateTime @default(now())
  assignedBy String

  @@id([postId, tagId])
}


你會發現一個多對多的關聯表,除了兩者 model 外,還會多一組一對多的 model ,如 _post_tag 關聯 posts 跟 tags ,這邊補充一下在 relation DB 中關聯表會是 _ 開頭當作他的 name

https://ithelp.ithome.com.tw/upload/images/20240927/20145677sgnotWd5oY.jpg

implicit

implicit 不代表不遵守 relation table 管理多對多的 table 的行為, 只是讓 prisma 自動幫你完成,讓你的 shcmea 更乾淨,無須多做額外的 shcmea 訂定,他可以讓你的 prisma client 使用時少一層 nested writes 如以下 create.assignedBy 跟 create.assignedAt 之後才是 create category

// explicit
const createCategory = await prisma.post.create({
  data: {
    title: 'How to be Bob',
    tags: {
      create: [
        {
          assignedBy: 'Bob',
          assignedAt: new Date(),
          category: {
            create: {
              name: 'New category',
            },
          },
        },
      ],
    },
  },
})

但在 implicit 就少一層~

// implicit
const createPostAndCategory = await prisma.post.create({
  data: {
    title: 'How to become a butterfly',
    tags: {
      create: [{ name: 'Magic' }, { name: 'Butterflies' }],
    },
  },
})
  • explicit : 讓關係表直接出現在 prisma 的 schema 中
  • implicit : 讓 prisma 自動幫你管理 relation table,而關係表不會在 prisma schema 中出現

這邊 prisma 比較推薦使用 implicit 的寫法,原因是如果你不需要在關係表中加上額外的欄位,例如何時產生關聯的,或是誰產生關係表的內容的話用 implicit 讓 prisma 自動幫你管理多對多的關係,但如果你要用 explicit 方式也是 OK 的

補充 如果要使用 implicit 的話有幾個點要注意:

  • 你不能使用 multi-field ID
model Tag {
  name String
  type String
  posts  TagsOnPosts[]
  @@id(fields: [name, type])
}
  • 不能用 @unique 取代 @id
model Tag {
  id Int @unique
  name String
  posts  TagsOnPosts[]
}

儘管不會跳 prisma error ,但如果你需要用的話只能在 explicit 中使用

Demo

Explicit many-to-many relations

Explicit 中 CategoriesOnPosts 作為中間表,是可以直接在 prisma client 中去使用,你可以訂定一些額外的欄位,同時組成 Explicit 的 schema 有三個

  • 兩個 model 分別為 Post 跟 Category 代表多對多關係
  • CategoriesOnPosts 作為一個關係表,裏頭訂定了關聯的關係欄位 (annotated relation) post 跟 category 去對應 postId 跟 categoryId

CategoriesOnPosts 很重要的是除了能管理 Post 跟 Category 的關聯外,也會提供額外的資訊例如 assignedAt 跟 assignedBy ,讓你 create Post 或是 create Category 關聯時知道是誰創建的資料,以及何時建立的資料等等

model Post {
  id         Int                 @id @default(autoincrement())
  title      String
  categories CategoriesOnPosts[]
}

model Category {
  id    Int                 @id @default(autoincrement())
  name  String
  posts CategoriesOnPosts[]
}

model CategoriesOnPosts {
  post       Post     @relation(fields: [postId], references: [id])
  postId     Int // relation scalar field (used in the `@relation` attribute above)
  category   Category @relation(fields: [categoryId], references: [id])
  categoryId Int // relation scalar field (used in the `@relation` attribute above)
  assignedAt DateTime @default(now())
  assignedBy String

  @@id([postId, categoryId])
}

備註: 所以多對多得關聯表會儲存兩個 model 的 FK

https://ithelp.ithome.com.tw/upload/images/20240927/20145677GIRCgZQHA8.jpg

上面的 schema 會產生以下的 SQL ,你會發現 CategoriesOnPosts 才會有 CONSTRAINT 去管理 FOREIGN KEY

CREATE TABLE "Post" (
    "id" SERIAL NOT NULL,
    "title" TEXT NOT NULL,

    CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);

CREATE TABLE "Category" (
    "id" SERIAL NOT NULL,
    "name" TEXT NOT NULL,

    CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);


-- Relation table + indexes --

CREATE TABLE "CategoriesOnPosts" (
    "postId" INTEGER NOT NULL,
    "categoryId" INTEGER NOT NULL,
    "assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT "CategoriesOnPosts_pkey" PRIMARY KEY ("postId","categoryId")
);

ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

CategoriesOnPosts 則是跟一對多規則是一樣的,因為 Post↔ CategoriesOnPosts and Category ↔ CategoriesOnPosts ,所以會有 @relation 去 annotated relation ,例如 categoryId 跟 postId

如果你不需要像是 assignedAt 這種額外的欄位在關係表中的話,直接使用 implicit 寫法就好

補充一下

  • 如果在 prisma 中使用 Multi-schema _CategoryToPost 必須要跟第一個 model 的 schema 要一樣的,也就是 Post 跟 _CategoryToPost 要一樣。

Explicit Querying

你會發現因為 Explicit 有多一層關係表的關係,所以在 create 會有三步驟

  1. create post
  2. create a record in the relation table CategoriesOnPosts
  3. create categories to associated Post
const createCategory = await prisma.post.create({
  data: {
    title: 'How to be Bob',
    categories: {
      create: [
        {
          assignedBy: 'Bob',
          assignedAt: new Date(),
          category: {
            create: {
              name: 'New category',
            },
          },
        },
      ],
    },
  },
})

讓你一樣也可以透過 connect 寫法,去 connect 已經有的 category

const assignCategories = await prisma.post.create({
  data: {
    title: 'How to be Bob',
    categories: {
      create: [
        {
          assignedBy: 'Bob',
          assignedAt: new Date(),
          category: {
            connect: {
              id: 9,
            },
          },
        },
        {
          assignedBy: 'Bob',
          assignedAt: new Date(),
          category: {
            connect: {
              id: 22,
            },
          },
        },
      ],
    },
  },
})

也可以使用 connectOrCreate 如果你不確定 category 是否存在

const assignCategories = await prisma.post.create({
  data: {
    title: 'How to be Bob',
    categories: {
      create: [
        {
          assignedBy: 'Bob',
          assignedAt: new Date(),
          category: {
            connectOrCreate: {
              where: {
                id: 9,
              },
              create: {
                name: 'New Category',
                id: 9,
              },
            },
          },
        },
      ],
    },
  },
})

這邊就是找出 category 是 New Category 的 post 你會發現因為 Explicit 關係會先有一層 categories 然後才是 category ,所以 Explicit 的寫法會有多一層 nested writes ,但如果你是 implicit 就會少這一層

const getPosts = await prisma.post.findMany({
  where: {
    categories: {
      some: {
        category: {
          name: 'New Category',
        },
      },
    },
  },
})

關係表也可以用來 query data 去檢查哪個 post 是透過 Bob 去創建的

const getAssignments = await prisma.categoriesOnPosts.findMany({
  where: {
    assignedBy: 'Bob',
    post: {
      id: {
        in: [9, 4, 10, 12, 22],
      },
    },
  },
})

Implicit many-to-many relations

Implicit 跟 Explicit 的差別只會在於 prisma ,但在 SQL 中的運行是沒有差異性的,唯一的差別是 Implicit 會讓 prisma 自動幫你管理如 Explicit mode 中需要創建 CategoriesOnPosts 這個 schema 這件事,所以在 Implicit mode 中你的 prisma 的 schema 並不會出現中間表,因為 Implicit 中你不需要額外的訊息去紀錄中間表這件事,所以 Implicit 會讓你的 prisma client 使用更簡單,以下是簡單的 Implicit demo:

model Post {
  id         Int        @id @default(autoincrement())
  title      String
  categories Category[]
}

model Category {
  id    Int    @id @default(autoincrement())
  name  String
  posts Post[]
}

Implicit Querying

這邊就是 create 一個 post 去關聯 categories

// Implicit
const createPostAndCategory = await prisma.post.create({
  data: {
    title: 'How to become a butterfly',
    categories: {
      create: [{ name: 'Magic' }, { name: 'Butterflies' }],
    },
  },
})

你會發現跟 Explicit 寫起來是不是乾淨很多呢XD

// Explicit
const assignCategories = await prisma.post.create({
  data: {
    title: 'How to be Bob',
    categories: {
      create: [
        {
          assignedBy: 'Bob',
          assignedAt: new Date(),
          category: {
            connect: {
              id: 9,
            },
          },
        },
        {
          assignedBy: 'Bob',
          assignedAt: new Date(),
          category: {
            connect: {
              id: 22,
            },
          },
        },
      ],
    },
  },
})

同樣也可以反過來去 create category

const createCategoryAndPosts = await prisma.category.create({
  data: {
    name: 'Stories',
    posts: {
      create: [
        { title: 'That one time with the stuff' },
        { title: 'The story of planet Earth' },
      ],
    },
  },
})

那如果是 get data 的話,因為 prisma 預設不會 return relation 的 data,所以如果要看到 relation data 的話記得加上 include

const getPostsAndCategories = await prisma.post.findMany({
  include: {
    categories: true,
  },
})

Rules for Implicit M-N Relation

  • 不使用 @relation 除非你需要 Disambiguating relations
Disambiguating relations demo
model User {
  id           Int     @id @default(autoincrement())
  name         String?
  writtenPosts Post[]
  pinnedPost   Post?
}

model Post {
  id         Int     @id @default(autoincrement())
  title      String?
  author     User    @relation(fields: [authorId], references: [id])
  authorId   Int
  pinnedBy   User?   @relation(fields: [pinnedById], references: [id])
  pinnedById Int?
}
  • 如果使用@relation 不能使用 references、fields、onUpdate、onDelete這些 arguments 因為在 Implicit 中會採用 default value
  • 兩個關聯的 model 必須要有 @id 不能使用 multi-field ID 的方式或是 @unique 取代 @id

Name Implicit M-N Relation

預設情況下 Post 跟 Category 的中間表他的 table name 會是 _CategoryToPost ,如果你希望 customer name 的話可以加在 @relation 中

model Post {
  id         Int        @id @default(autoincrement())
  categories Category[] @relation("MyRelationTable")
}

model Category {
  id    Int    @id @default(autoincrement())
  posts Post[] @relation("MyRelationTable")
}

總結

所以在 create 多對多的 relation table 中,在 SQL 的模型會是將關聯兩個不同的 entities ,在這個模型的本質上他會是兩個 1-n 的 relation table 組合起來的,也就是上面說的關係表的概念 (CategoriesOnPosts) 。

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

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


上一篇
Day12. 一些讓你看來很強的 ORM - prisma (relation)1-n
下一篇
Day14. 一些讓你看來很強的 ORM - prisma (Fluent API)
系列文
一些讓你看來很強的 ORM - prisma 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言