arrow_back返回文章列表
技术

第 4 篇:Prisma 关系建模与查询实战(blog-demo 项目)

从这一篇开始,我们从只有 User 的单表世界,走向有真实业务语义的多表世界:博客系统 blog-demo

我们会用 Prisma 建出一套经典关系模型:User / Profile / Post / Comment / Category,并围绕它讲清楚:

本篇默认你已经熟悉前面讲过的 Schema 基础语法(datasource / generator / 标量类型 / 属性),如果忘了可以随时翻回第 3 篇。


1. blog-demo 项目与整体模型设计

我们继续沿用前几篇的技术栈心智模型:

本篇的目标数据模型是一个简化博客系统:

最终会形成下面这样的关系:


2. blog-demo 的 schema.prisma:一口气写完,再逐段拆开

先给出一个完整的 schema 版本,然后我们分块解释每种关系写法。

// generator & datasource:与前几篇保持一致的风格
generator client {
  provider = "prisma-client"
  output   = "../generated/blog"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// 用户表
model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt

  // 关系字段
  profile   Profile?
  posts     Post[]
  comments  Comment[]
}

// 用户档案(一对一)
model Profile {
  id        Int     @id @default(autoincrement())
  bio       String?
  avatarUrl String?

  userId    Int     @unique
  user      User    @relation(fields: [userId], references: [id])
}

// 文章(一对多:User -> Post)
model Post {
  id        Int        @id @default(autoincrement())
  title     String
  content   String?
  published Boolean     @default(false)
  createdAt DateTime    @default(now())
  updatedAt DateTime    @updatedAt

  authorId  Int
  author    User        @relation(fields: [authorId], references: [id])

  comments  Comment[]
  categories PostCategory[]
}

// 评论(一对多:Post -> Comment;User -> Comment)
model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  createdAt DateTime @default(now())

  postId    Int
  post      Post     @relation(fields: [postId], references: [id])

  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
}

// 分类(多对多:Post <-> Category)
model Category {
  id        Int            @id @default(autoincrement())
  name      String
  slug      String         @unique

  posts     PostCategory[]
}

// 显式中间表(多对多 + 额外字段的预留空间)
model PostCategory {
  postId     Int
  categoryId Int

  assignedAt DateTime @default(now())

  post       Post     @relation(fields: [postId], references: [id])
  category   Category @relation(fields: [categoryId], references: [id])

  @@id([postId, categoryId])
}

这个 schema 涵盖了:

下面我们按关系类型拆开解释。


3. 一对一关系:User – Profile

3.1 一对一的概念与建模选择

一对一=一个用户有一个档案,一个档案属于一个用户。常见场景:鉴权相关字段在 User,扩展信息在 Profile。

在 Prisma 里,一对一关系的写法和“一对多”的语法很像,只是关系字段那一侧用单数+非数组

3.2 Schema 写法拆解

model User {
  // ...
  profile   Profile?
}

model Profile {
  id        Int     @id @default(autoincrement())
  bio       String?
  avatarUrl String?

  userId    Int     @unique
  user      User    @relation(fields: [userId], references: [id])
}

关键点:

  1. 在 Profile 上有一个外键字段 userId Int,并且加了 @unique
    这保证了“一个 Profile 只对上一个 User”,防止多档案绑同一个用户。

  2. 关系字段(对象类型):user User
    使用 @relation(fields: [userId], references: [id]) 指明:

  1. User 这边的关系字段:profile Profile?

    • Profile? 表示可选(一部分用户可能还没有档案);

    • 不需要再写 @relation,因为 Prisma 可以从反向推断。

3.3 查询与嵌套写入示例

查询某用户和他的 Profile

const userWithProfile = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    profile: true,
  },
})

在创建 User 时同时创建 Profile(嵌套写入)

const user = await prisma.user.create({
  data: {
    email: 'bob@example.com',
    name: 'Bob',
    profile: {
      create: {
        bio: 'I love Prisma',
        avatarUrl: 'https://example.com/avatar.png',
      },
    },
  },
  include: { profile: true },
})

要点:


4. 一对多关系:User – Post,Post – Comment

4.1 一对多的通用模式

一对多的核心是:

4.2 User – Post(一个用户多篇文章)

model User {
  // ...
  posts    Post[]
}

model Post {
  id        Int        @id @default(autoincrement())
  title     String
  content   String?
  published Boolean     @default(false)
  createdAt DateTime    @default(now())
  updatedAt DateTime    @updatedAt

  authorId  Int
  author    User        @relation(fields: [authorId], references: [id])

  comments  Comment[]
  categories PostCategory[]
}

要点:

查询某用户及其文章

const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: true,
  },
})

查询文章时一并带出作者

const posts = await prisma.post.findMany({
  include: {
    author: true,
  },
})

创建 User 的同时创建多篇 Post(嵌套写入)

const user = await prisma.user.create({
  data: {
    email: 'writer@example.com',
    name: 'Writer',
    posts: {
      create: [
        { title: '第一篇文章', content: 'Hello Prisma' },
        { title: '第二篇文章', content: '关系建模真好玩' },
      ],
    },
  },
  include: { posts: true },
})

给已有用户新增一篇文章(连接已有关系)

const post = await prisma.post.create({
  data: {
    title: '新文',
    content: '内容...',
    author: {
      connect: { id: 1 }, // 连接已有用户
    },
  },
})

这里体现了 Prisma 嵌套写入里的两个常用操作:

4.3 Post – Comment(文章的评论)

model Post {
  // ...
  comments  Comment[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  createdAt DateTime @default(now())

  postId    Int
  post      Post     @relation(fields: [postId], references: [id])

  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
}

Comment 同时连着 Post 和 User(一条评论属于某篇文章,也属于某个用户),所以有两个外键:postIdauthorId

为某篇文章创建一条评论

const comment = await prisma.comment.create({
  data: {
    content: '写得不错!',
    post: {
      connect: { id: 123 }, // 关联文章
    },
    author: {
      connect: { id: 1 },   // 关联用户
    },
  },
})

查询一篇文章及其评论(附带作者)

const postWithComments = await prisma.post.findUnique({
  where: { id: 123 },
  include: {
    comments: {
      include: {
        author: true,
      },
    },
  },
})

这里有一个嵌套 include:


5. 多对多关系:Post – Category

5.1 多对多的两种建模方式

在 Prisma 里,多对多关系有两种常见方式:

  1. 隐式多对多(implicit many-to-many)
    只在两个模型上写 categories Category[]posts Post[],Prisma 帮你自动创建中间表。

  2. 显式多对多(explicit many-to-many)
    手动创建中间表模型,例如 PostCategory,适合中间表需要额外字段(比如排序、打分、时间戳)时。

本篇为了通用性,采用显式中间表方式(更灵活)。

5.2 Schema 写法:Category + PostCategory

model Category {
  id        Int            @id @default(autoincrement())
  name      String
  slug      String         @unique

  posts     PostCategory[]
}

model PostCategory {
  postId     Int
  categoryId Int

  assignedAt DateTime @default(now())

  post       Post     @relation(fields: [postId], references: [id])
  category   Category @relation(fields: [categoryId], references: [id])

  @@id([postId, categoryId])
}

要点:

5.3 为文章添加分类

1)先创建分类

const tech = await prisma.category.create({
  data: {
    name: '技术',
    slug: 'tech',
  },
})
const life = await prisma.category.create({
  data: {
    name: '生活',
    slug: 'life',
  },
})

2)把某篇文章挂到多个分类下

const postCategoryLinks = await prisma.postCategory.createMany({
  data: [
    { postId: 123, categoryId: tech.id },
    { postId: 123, categoryId: life.id },
  ],
  skipDuplicates: true,
})

5.4 查询:文章及其分类 / 分类下的文章

查文章时附带分类

const post = await prisma.post.findUnique({
  where: { id: 123 },
  include: {
    categories: {
      include: {
        category: true,
      },
    },
  },
})

返回结构类似:

在实际项目里,如果你觉得这太啰嗦,可以在应用层做一层映射,把 PostCategory 转成更简单的 { id, name, slug } 类型给前端用。

查某个分类下的所有文章

const categoryWithPosts = await prisma.category.findUnique({
  where: { slug: 'tech' },
  include: {
    posts: {
      include: {
        post: true,
      },
    },
  },
})

同理,这里是 Category.posts(PostCategory[]),每个元素里再有 post 字段。


6. 关系查询里的几个关键概念:include / select / relation filter

6.1 include vs select:要什么数据、拿多少字段?

例子:查文章时只拿作者的 name,而不拿作者的所有字段:

const posts = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    author: {
      select: {
        id: true,
        name: true,
      },
    },
  },
})

相反,如果你只是想多带几个关联,但字段懒得筛,只要 include: { author: true, comments: true } 就够用。

6.2 relation filter:按关联条件过滤

Prisma Client 支持按关系条件过滤,比如:“找出有已发布文章的用户”,“找出有超过 3 条评论的文章”等。

例如:找出拥有至少一篇 published 文章的用户:

const usersWithPublished = await prisma.user.findMany({
  where: {
    posts: {
      some: {
        published: true,
      },
    },
  },
})

这里的 posts.some 是典型 relation filter 的写法:

再比如:找出没有任何评论的文章(没有“被讨论”的文章):

const postsWithoutComments = await prisma.post.findMany({
  where: {
    comments: {
      none: {}, // 没有任何 comment 即可
    },
  },
})

7. 嵌套写入的几个常见模式

你在实际代码中,很少手动分步做“先插入 User,再插入 Post,再手写外键”,而是更常用 Prisma 的嵌套写入能力:

常见模式:

典型例子:注册一个用户的同时,创建 Profile 并发一篇欢迎文章。

const user = await prisma.user.create({
  data: {
    email: 'newuser@example.com',
    name: 'Newbie',
    profile: {
      create: {
        bio: '新来的',
      },
    },
    posts: {
      create: {
        title: 'Hello Blog',
        content: '我的第一篇博客',
        published: true,
      },
    },
  },
  include: {
    profile: true,
    posts: true,
  },
})

一个 API 请求,一次事务,多张表一起写完,同时 Client 返回完整的嵌套数据。


8. 这一篇之后,你已经可以为任何“小型业务”建出一套像样的关系模型

到这里,你已经掌握了 Prisma 关系建模里的几个关键技法: