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 提供 explicitimplicit 兩種方式管理 schema

explicit

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

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 關聯 poststags ,這邊補充一下在 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.assignedBycreate.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 : 讓關係表直接出現在 prismaschema
  • implicit : 讓 prisma 自動幫你管理 relation table,而關係表不會在 prisma schema 中出現

這邊 prisma 比較推薦使用 implicit 的寫法,原因是如果你不需要在關係表中加上額外的欄位,例如何時產生關聯的,或是誰產生關係表的內容的話用 implicitprisma 自動幫你管理多對多的關係,但如果你要用 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

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

  • 兩個 model 分別為 PostCategory 代表多對多關係
  • CategoriesOnPosts 作為一個關係表,裏頭訂定了關聯的關係欄位 (annotated relation) postcategory 去對應 postIdcategoryId

CategoriesOnPosts 很重要的是除了能管理 PostCategory 的關聯外,也會提供額外的資訊例如 assignedAtassignedBy ,讓你 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])
}

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

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 ,所以會有 @relationannotated relation ,例如 categoryIdpostId

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

補充一下

  • 如果在 prisma 中使用 Multi-schema _CategoryToPost 必須要跟第一個 modelschema 要一樣的,也就是 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,
              },
            },
          },
        },
      ],
    },
  },
})

這邊就是找出 categoryNew Categorypost 你會發現因為 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

ImplicitExplicit 的差別只會在於 prisma ,但在 SQL 中的運行是沒有差異性的,唯一的差別是 Implicit 會讓 prisma 自動幫你管理如 Explicit mode 中需要創建 CategoriesOnPosts 這個 schema 這件事,所以在 Implicit mode 中你的 prismaschema 並不會出現中間表,因為 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 relationdata,所以如果要看到 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 不能使用 referencesfieldsonUpdateonDelete這些 arguments 因為在 Implicit 中會採用 default value
  • 兩個關聯的 model 必須要有 @id 不能使用 multi-field ID 的方式或是 @unique 取代 @id

Name Implicit M-N Relation

預設情況下 PostCategory 的中間表他的 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-nrelation table 組合起來的,也就是上面說的關係表的概念 (CategoriesOnPosts)

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

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


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

尚未有邦友留言

立即登入留言