详解 Primsa 中数据库表之间关系

3,105 阅读9分钟

一、简介

详解 Primsa 中数据库表之间的关系 .png

1.1) 数据库中的关系

关系是 Prisma 模式中两个模型之间的链接。

1.2) Primsa 中的关系

在 Prisma 中通过关系字段外键以及@relation关联不同的模型。

1.3) 关系类型

1.4)显示和隐式关系

1.5) 关系字段

model User {
  id    Int    @id @default(autoincrement())
  role  Role   @default(USER)
  posts Post[]
}

model Post {
  id       Int    @id @default(autoincrement())
  author   User   @relation(fields: [authorId], references: [id])
  authorId Int // relation scalar field (used in the `@relation` attribute above)
}

@relation 表示为关系字段。其中 User 模型的关系字段是 posts, Post 模型的关系字段是 user

1.6)@relation 关系函数

@relation 类似一个函数可以传递的参数:

  • name 标记名称
  • fields 当前模型的字段列表(一般使用 id)
  • references 关联模型的字段列表
  • map 自定义名字
  • onUpdate 定义更新关联 action
  • onDelete 定义删除 action

1.7) 使用 name 消除歧义

  • name 是第一个参数。
model User {
  id           Int     @id @default(autoincrement())
  writtenPosts Post[]  @relation("WrittenPosts")
  pinnedPost   Post?   @relation("PinnedPost")
}

model Post {
  id         Int     @id @default(autoincrement())
  author     User    @relation("WrittenPosts", fields: [authorId], references: [id])
  authorId   Int
  pinnedBy   User?   @relation("PinnedPost", fields: [pinnedById], references: [id])
  pinnedById Int?    @unique
}

如果不适用 name 当关系比较复杂时候,很难一眼看出关联关系。在 @relation 中使用 name,可以消除歧义。

二、一对一关系

用户用户详情是一个常见的一对一关系,以下会以这种 User/Profile 的模型。

2.1) 关系型

model User {
  id      Int      @id @default(autoincrement())
  profile Profile?
}

model Profile {
  id     Int  @id @default(autoincrement())
  user   User @relation(fields: [userId], references: [id])
  userId Int  @unique
}

用户中的 profileProfile 中的 user 构成一对一的关联关系。

2.2) 非关系型 MongoDB

model User {
  id      String   @id @default(auto()) @map("_id") @db.ObjectId
  profile Profile?
}

model Profile {
  id     String @id @default(auto()) @map("_id") @db.ObjectId
  user   User   @relation(fields: [userId], references: [id])
  userId String @unique @db.ObjectId
}

MongoDB 与关系型数据库使用类似,更具 MongoDB 的特点,类型,属性发生一些变化。例如 MongoDB 中的 id 用字符串笔记合理。

2.3) 可选性

在一对一关系中,没有关系标量的关系一侧(表示数据库中外键的字段)必须是可选的:

profile Profile?

2.4) 关联位置选择

关联位置是可选的,将以上的例子交换关联位置, 在 Uesr 中进行关联:

model User {
  id        Int      @id @default(autoincrement())
  profile   Profile? @relation(fields: [profileId], references: [id])
  profileId Int?     @unique // relation scalar field (used in the `@relation` attribute above)
}

model Profile {
  id   Int   @id @default(autoincrement())
  user User?
}

三、一对多关系

用户用户发表的内容(文章、博客Post)是一个常见的一对一关系,以下会以这种 User/Post 的模型。

多字段关系

一对一类似, 使用 @relation 标记和模型数组。

关系型

model User {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

model Post {
  id       Int  @id @default(autoincrement())
  author   User @relation(fields: [authorId], references: [id])
  authorId Int
}

User 模型中的 posts 字段与 Post 模型中的 author 字段配合 @relation 进行关联。注意 posts 类型是 Post[]列表形式。

非关系型 MongoDB

model User {
  id    String @id @default(auto()) @map("_id") @db.ObjectId
  posts Post[]
}

model Post {
  id       String @id @default(auto()) @map("_id") @db.ObjectId
  author   User   @relation(fields: [authorId], references: [id])
  authorId String @db.ObjectId
}

非关系型 MongoDB 的用法与关系型基本一致,类型上有所区别。

可选性

注意以上 Post 示例没有标记为可选的,但是在多对多的关系中,是可以标记为可选的。

model User {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

model Post {
  id       Int   @id @default(autoincrement())
  author   User? @relation(fields: [authorId], references: [id])
  authorId Int?
}

可选的意味着,在创建的时候可以不分配内容。

四、多对多关系

多对多关系: 关系一侧的零个或多个记录可以连接到另一侧的零个或多个记录的关系。

显式多对多关系

  • 显示定义多对多一般需要三个模型(包含一个中间模型)
  • 一种表示关系表的模型,例如底层数据库中的关系表(有时也称为JOIN链接数据透视表)。

例如:博客的 Post 与博客分类 Category, 通过 CategoriesOnPosts 中间模型进行关联。

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])
}

需要注意的是: Post/Category 模型需要与 CategoriesOnPosts 中间模型也进行关联。

隐式多对多关系

与 显示不同的是,隐式的没有中间(或者隐藏了)以下是简单的示例

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

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

没有中间模型,之后 Post 与 Category 直接进行关联,并且没有 @relation 关联属性。表示多对多关系。

关系表

  • 关系表(有时也称为JOIN、链接或数据透视表)连接两个或多个其他表,从而在它们之间创建关系。

  • 创建关系表是 SQL 中常见的数据建模实践,用于表示不同实体之间的关系。

  • 本质上,它意味着 "在数据库中将一个多对多关系建模为两个 一对多关系"。

  • 建议使用隐式多对多关系,其中 Prisma 自动在底层数据库中生成关系表。

  • 当需要在关系中存储附加数据(例如创建关系的日期)时,应使用显式多对多关系。

MongoDB

MongoDB 与关系型数据不同:

  • 需要 @relation 属性
  • 带有强制的 fieldsreferences 参数
  • 每侧引用对应的 ID
model Post {
  id          String     @id @default(auto()) @map("_id") @db.ObjectId
  categoryIDs String[]   @db.ObjectId
  categories  Category[] @relation(fields: [categoryIDs], references: [id])
}

model Category {
  id      String   @id @default(auto()) @map("_id") @db.ObjectId
  name    String
  postIDs String[] @db.ObjectId
  posts   Post[]   @relation(fields: [postIDs], references: [id])
}

五、自我关系

在数据中有一类特殊的,类似于 树形结构 数据需要处理。以下会以 菜单关注与被关注为例讲解

一对一的自我关系

model MenuItem {
  id        Int      @id @default(autoincrement())
  label     String
  parent    MenuItem? @relation("ChildToParent", fields: [parentId], references: [id])
  parentId  Int?
  children  MenuItem[] @relation("ChildToParent")
}

子路由 children 只能有一个 parent 父路由,构成了 自我关系的 一对一关系。

一对多的自关系

model MenuItem {
  id        Int      @id @default(autoincrement())
  label     String
  parent    MenuItem? @relation("ChildToParent", fields: [parentId], references: [id])
  parentId  Int?
  children  MenuItem[] @relation("ChildToParent")
}

parent 中包含了 children 可以是多个, 构成 自我关系 一对多的关系。

多对多自关系

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

作为一个用户,可以被多个人关注,作为用户,也可以关注很多人,这是多对多关系。为了消除歧义,此时使用 @relation属性进行标记, 当然值得主要是这是 多对多关系中的隐式关系。

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

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

  @@id([followerId, followingId])
}

六、关系中的 Action

onDelete

用于定义在父记录(包含外键的记录)被删除时,与该父记录相关联的子记录的行为。它控制了删除操作对关联子记录的影响

onUpdate

用于定义在父记录的主键值被更新时,与该父记录相关联的子记录的行为。它控制了更新操作对关联子记录的影响

Cascade

  • delete 中:表示当父记录被删除时,相关联的子记录也会被删除。
  • update 中:表示当父记录的主键值被更新时,相关联的子记录也会相应地更新外键字段。

Restrict

  • delete 中:外键约束操作选项,表示当尝试删除新父记录时,如果有关联的子记录存在,将会限制(阻止)这些操作。
  • update 中:外键约束操作选项,表示当尝试更新父记录时,如果有关联的子记录存在,将会限制(阻止)这些操作。

Restrict 选项时,你必须先删除或更新子记录,然后才能删除或更新父记录。

NoAction

  • delete 中:表示不执行任何操作,保留外键约束,不允许删除父记录,直到相关的子记录被删除或更新。
  • update 中:表示不执行任何操作,保留外键约束,不允许更新父记录的主键值,直到相关的子记录被删除或更新。

SetNull

  • delete 中:表示当父记录被删除时,相关联的子记录的外键字段将被设置为 NULL
  • update 中:表示当父记录的主键值被更新时,相关联的子记录的外键字段将被设置为 NULL

SetDefault

  • delete 中:外键约束操作选项,表示当尝试删除父记录时,将会将子记录的外键字段设置为其默认值。
  • update 中:外键约束操作选项,表示当尝试更新父记录时,将会将子记录的外键字段设置为其默认值。

默认值是在创建表格时定义的,通常是一个合适的默认值或 NULL,具体取决于表格设计。

七、关系后迁移

如果你的schema 发生变化使用 db push 命令同步数据模型

npx prisma db push
  • 创建新的数据表
  • 更新表
  • 数据迁移
  • 更新外键约束

八、关系模式

foreignKeys(默认)

foreignKeys 模式

特点描述
外键约束依赖数据库层面的外键约束来维护数据关系的完整性和一致性。
外键字段创建外键字段在数据库表格中创建,用于明确表示关系。
删除和更新行为使用 onDeleteonUpdate 选项定义在删除或更新父记录时如何处理相关子记录。
适用场景适用于需要强制执行数据库级别外键约束的情况,以确保数据完整性。

prisma

特点描述
数据库无外键约束不依赖数据库外键约束,关系通过 Prisma 数据模型定义,外键字段不一定在数据库中创建。
Prisma Client 管理Prisma Client 负责在应用程序层面管理关系,包括创建、查询、更新和删除关联记录。
灵活性和控制提供更多灵活性和控制,可以使用 includeselect 等查询选项来检索和操作关联数据。
适用场景适用于不依赖数据库外键约束,而是在应用程序层面管理关系的情况,提供更多灵活性和抽象性。
  • 使用
datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"
}

九、小结

本文主要讲解了 Primsa 中的数据模型之间的关系,从 一对一一对多多对多,到后面的 自我关系,到最后的 Action 和数据迁移模式讲解,全面的概括了 Prisma 中模型之间的关系。Prisma + typescript 在数据库操作方面,高效方便,非常值得学习。