跳至主要内容

从 Mongoose 迁移

本指南介绍了如何从 Mongoose 迁移到 Prisma ORM。它使用扩展版的 Mongoose Express 示例 作为 示例项目 来演示迁移步骤。您可以在 GitHub 上找到此指南中使用的示例。

您可以在 Prisma ORM 与 Mongoose 页面上了解 Prisma ORM 与 Mongoose 的比较方式。

迁移过程概述

请注意,从 Mongoose 迁移到 Prisma ORM 的步骤始终相同,无论您正在构建哪种应用程序或 API 层。

  1. 安装 Prisma CLI
  2. 内省您的数据库
  3. 安装并生成 Prisma 客户端
  4. 逐步用 Prisma 客户端替换您的 Mongoose 查询

无论您是在构建 REST API(例如使用 Express、koa 或 NestJS)、GraphQL API(例如使用 Apollo Server、TypeGraphQL 或 Nexus)还是任何其他使用 Mongoose 进行数据库访问的应用程序,这些步骤都适用。

Prisma ORM 非常适合 **增量采用**。这意味着,您无需一次性将整个项目从 Mongoose 迁移到 Prisma ORM,而是可以**逐步**将您的数据库查询从 Mongoose 迁移到 Prisma ORM。

示例项目概述

在本指南中,我们将使用一个用 Express 构建的 REST API 作为 示例项目 迁移到 Prisma ORM。它包含三个文档和一个子文档(嵌入式文档)。

const mongoose = require('mongoose')

const Schema = mongoose.Schema

const PostSchema = new Schema({
title: String,
content: String,
published: {
type: Boolean,
default: false,
},
author: {
type: Schema.Types.ObjectId,
ref: 'author',
required: true,
},
categories: [
{
type: Schema.Types.ObjectId,
ref: 'Category',
},
],
})

module.exports = mongoose.model('Post', PostSchema)

模型/文档具有以下类型的关系

  • 1-n: UserPost
  • m-n: PostCategory
  • 子文档/嵌入式文档: UserProfile

在本指南中使用的示例中,路由处理程序位于 src/controllers 目录中。模型位于 src/models 目录中。从那里,模型被拉入一个中心 src/routes.js 文件,该文件用于在 src/index.js 中定义所需的路由。

└── blog-mongoose
├── package.json
└──src
   ├── controllers
   │   ├── post.js
   │   └── user.js
   ├── models
   │   ├── category.js
   │   ├── post.js
   │   └── user.js
   ├── index.js
   ├── routes.js
   └── seed.js

示例存储库在 package.json 文件中包含一个 seed 脚本。

运行 npm run seed 以使用 ./src/seed.js 文件中的示例数据填充您的数据库。

步骤 1. 安装 Prisma CLI

采用 Prisma ORM 的第一步是在您的项目中 安装 Prisma CLI

npm install prisma --save-dev

步骤 2. 内省您的数据库

内省是检查数据库结构的过程,在 Prisma ORM 中使用它来生成 数据模型 到您的 Prisma 架构 中。

2.1. 设置 Prisma

在内省您的数据库之前,您需要设置您的 Prisma 架构 并将 Prisma ORM 连接到您的数据库。在您的终端中运行以下命令以创建一个基本的 Prisma 架构文件。

npx prisma init --datasource-provider mongodb

此命令创建

  • 一个名为 prisma 的新目录,其中包含一个 schema.prisma 文件;您的 Prisma 架构指定了您的数据库连接和模型。
  • .env: 一个 dotenv 文件,位于您项目的根目录(如果它还不存在),用于配置您的数据库连接 URL 作为环境变量。

Prisma 架构目前如下所示

prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

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

generator client {
provider = "prisma-client-js"
}

提示

为了在使用 Prisma ORM 时获得最佳的开发体验,请参考 编辑器设置,了解语法高亮、格式化、自动完成以及更多酷炫的功能。

2.2. 连接您的数据库

.env 文件中配置您的 数据库连接 URL

Mongoose 使用的连接 URL 格式类似于 Prisma ORM 使用的格式。

.env
DATABASE_URL="mongodb://alice:myPassword43@localhost:27017/blog-mongoose"

有关更多详细信息,请参考 MongoDB 连接 URL 规范

2.3. 运行 Prisma ORM 的内省

连接 URL 就位后,您可以 内省 您的数据库以生成 Prisma 模型

注意: MongoDB 是一个无模式数据库。为了在您的项目中增量采用 Prisma ORM,请确保您的数据库已填充了示例数据。Prisma ORM 通过对存储的数据进行采样并从数据库中的数据推断模式来内省 MongoDB 模式。

npx prisma db pull

这将创建以下 Prisma 模型

prisma/schema.prisma
type UsersProfile {
bio String
}

model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
}

model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
author String @db.ObjectId
categories String[] @db.ObjectId
content String
published Boolean
title String
}

model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
}

生成的 Prisma 模型表示 MongoDB 集合,是您程序化 Prisma 客户端 API 的基础,它允许您向您的数据库发送查询。

2.4. 更新关系

MongoDB 不支持不同集合之间的关系。但是,您可以使用 ObjectId 字段类型或从一个文档到多个文档,使用集合中 ObjectId 的数组,在文档之间创建引用。引用将存储相关文档的 id(s)。您可以使用 Mongoose 提供的 populate() 方法,将引用填充为相关文档的数据。

按如下方式更新 Post <-> User 之间的 1-n 关系

  • posts 模型中现有的 author 引用重命名为 authorId 并添加 @map("author") 属性
  • posts 模型中添加 author 关系字段,以及它的 @relation 属性,指定 fieldsreferences
  • users 模型中添加 posts 关系
type UsersProfile {
bio String
}

model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
}

model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean
v Int @map("__v")

- author String @db.ObjectId
+ author users @relation(fields: [authorId], references: [id])
+ authorId String @map("author") @db.ObjectId

categories String[] @db.ObjectId
}

model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
+ posts posts[]
}

按如下方式更新 Post <-> Category 之间的 m-n 引用

  • posts 模型中的 categories 字段重命名为 categoryIds 并使用 @map("categories") 映射它
  • posts 模型中添加一个新的 categories 关系字段
  • categories 模型中添加 postIds 标量列表字段
  • categories 模型中添加 posts 关系
  • 在两个模型上都添加一个 关系标量
  • 在两边添加 @relation 属性,指定 fieldsreferences 参数
type UsersProfile {
bio String
}

model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
name String
+ posts posts[] @relation(fields: [postIds], references: [id])
+ postIds String[] @db.ObjectId
}

model posts {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean
v Int @map("__v")

author users @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId

- categories String[] @db.ObjectId
+ categories categories[] @relation(fields: [categoryIds], references: [id])
+ categoryIds String[] @map("categories") @db.ObjectId
}

model users {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @map("__v")
email String @unique(map: "email_1")
name String
profile UsersProfile?
posts posts[]
}

2.5. 调整 Prisma 架构(可选)

通过内省生成的模型目前与您的数据库集合完全匹配。在本节中,您将了解如何调整 Prisma 模型的命名以符合 Prisma ORM 的命名约定

这些调整中的一部分完全是可选的,如果您现在不想进行任何调整,可以随意跳到下一步。您可以在稍后的任何时间点返回并进行调整。

与 Prisma 模型当前的 snake_case 表示法相反,Prisma ORM 的命名约定是

  • PascalCase 用于模型名称
  • camelCase 用于字段名称

您可以通过 映射 Prisma 模型和字段名称到基础数据库中现有的表和列名称来调整命名,分别使用 @@map@map

提示

您可以使用 重命名符号 操作通过突出显示模型名称、按下 F2 键并最后键入所需的名称来重构模型名称。这将重命名它被引用的所有实例,并将 @@map() 属性添加到具有其先前名称的现有模型中。

如果您的模式包含 versionKey,请通过在 v 字段中添加 @default(0)@ignore 属性来更新它。这意味着该字段将从生成的 Prisma Client 中排除,并且将具有默认值 0。Prisma ORM 不处理文档版本控制。

还要注意,您可以重命名 关系字段 以优化您稍后将用于向数据库发送查询的 Prisma Client API。例如,user 模型上的 post 字段是一个列表,因此该字段的更好名称是 posts,以表明它是复数形式。

通过包括 @default 属性来更新 published 字段以定义该字段的默认值。

您也可以将 UserProfile 复合类型重命名为 Profile

这是解决这些问题的 Prisma 模式调整版本

prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}

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

type Profile {
bio String
}

model Category {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
v Int @default(0) @map("__v") @ignore

posts Post[] @relation(fields: [post_ids], references: [id])
post_ids String[] @db.ObjectId

@@map("categories")
}

model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
content String
published Boolean @default(false)
v Int @default(0) @map("__v") @ignore

author User @relation(fields: [authorId], references: [id])
authorId String @map("author") @db.ObjectId

categories Category[] @relation(fields: [categoryIds], references: [id])
categoryIds String[] @db.ObjectId

@@map("posts")
}

model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
v Int @default(0) @map("__v") @ignore
email String @unique(map: "email_1")
name String
profile Profile?
posts Post[]

@@map("users")
}

步骤 3. 安装 Prisma Client

下一步,您可以在项目中安装 Prisma Client,以便您可以开始替换当前使用 Mongoose 进行的项目中的数据库查询

npm install @prisma/client

步骤 4. 将您的 Mongoose 查询替换为 Prisma Client

在本节中,我们将根据示例 REST API 项目中的示例路由,展示一些从 Mongoose 迁移到 Prisma Client 的示例查询。有关 Prisma Client API 与 Mongoose 不同的全面概述,请查看 Mongoose 和 Prisma API 比较 页面。

首先,要设置您将用于从各种路由处理程序发送数据库查询的 PrismaClient 实例,请在 src 目录中创建一个名为 prisma.js 的新文件

touch src/prisma.js 

现在,实例化 PrismaClient 并将其从文件中导出,以便您稍后在路由处理程序中使用它

src/prisma.js
const { PrismaClient } = require('@prisma/client')

const prisma = new PrismaClient()

module.exports = prisma

我们控制器文件中的导入如下

src/controllers/post.js
const Post = require('../models/post')
const User = require('../models/user')
const Category = require('../models/category')
src/controllers/user.js
const Post = require('../models/post')
const User = require('../models/user')

在从 Mongoose 迁移到 Prisma 时,您将更新控制器导入

src/controllers/post.js
const prisma = require('../prisma')
src/controllers/user.js
const prisma = require('../prisma')

4.1. 替换 GET 请求中的查询

本指南中使用的示例 REST API 有四个接受 GET 请求的路由

  • /feed?searchString={searchString}&take={take}&skip={skip}:返回所有已发布的帖子
    • 查询参数(可选)
      • searchString:按 titlecontent 筛选帖子
      • take:指定列表中应返回多少个对象
      • skip:指定应跳过多少个返回的对象
  • /post/:id:返回特定帖子
  • /authors:返回作者列表

让我们深入研究实现这些请求的路由处理程序。

/feed

/feed 处理程序的实现方式如下

src/controllers/post.js
const feed = async (req, res) => {
try {
const { searchString, skip, take } = req.query

const or =
searchString !== undefined
? {
$or: [
{ title: { $regex: searchString, $options: 'i' } },
{ content: { $regex: searchString, $options: 'i' } },
],
}
: {}

const feed = await Post.find(
{
...or,
published: true,
},
null,
{
skip,
batchSize: take,
}
)
.populate({ path: 'author', model: User })
.populate('categories')

return res.status(200).json(feed)
} catch (error) {
return res.status(500).json(error)
}
}

请注意,每个返回的 Post 对象都包含与之关联的 authorcategory 的关系。使用 Mongoose,包括关系不是类型安全的。例如,如果您在检索到的关系中出现拼写错误,您的数据库查询只会在运行时失败——JavaScript 编译器在这里不提供任何安全性。

以下是使用 Prisma Client 实现相同路由处理程序的方式

src/controllers/post.js
const feed = async (req, res) => {
try {
const { searchString, skip, take } = req.query

const or = searchString
? {
OR: [
{ title: { contains: searchString } },
{ content: { contains: searchString } },
],
}
: {}

const feed = await prisma.post.findMany({
where: {
published: true,
...or,
},
include: { author: true, categories: true },
take: Number(take) || undefined,
skip: Number(skip) || undefined,
})

return res.status(200).json(feed)
} catch (error) {
return res.status(500).json(error)
}
}

请注意,Prisma Client 包括 author 关系的方式绝对是类型安全的。如果您尝试包括 Post 模型上不存在的关系,JavaScript 编译器将抛出错误。

/post/:id

/post/:id 处理程序的实现方式如下

src/controllers/post.js
const getPostById = async (req, res) => {
const { id } = req.params

try {
const post = await Post.findById(id)
.populate({ path: 'author', model: User })
.populate('categories')

if (!post) return res.status(404).json({ message: 'Post not found' })

return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}

使用 Prisma ORM,路由处理程序的实现方式如下

src/controllers/post.js
const getPostById = async (req, res) => {
const { id } = req.params

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

if (!post) return res.status(404).json({ message: 'Post not found' })

return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}

4.2. 替换 POST 请求中的查询

REST API 有三个接受 POST 请求的路由

  • /user:创建一个新的 User 记录
  • /post:创建一个新的 User 记录
  • /user/:id/profile:为具有给定 ID 的 User 记录创建一个新的 Profile 记录

/user

/user 处理程序的实现方式如下

src/controllers/user.js
const createUser = async (req, res) => {
const { name, email } = req.body

try {
const user = await User.create({
name,
email,
})

return res.status(201).json(user)
} catch (error) {
return res.status(500).json(error)
}
}

使用 Prisma ORM,路由处理程序的实现方式如下

src/controllers/user.js
const createUser = async (req, res) => {
const { name, email } = req.body

try {
const user = await prisma.user.create({
data: {
name,
email,
},
})

return res.status(201).json(user)
} catch (error) {
return res.status(500).json(error)
}
}

/post

/post 处理程序的实现方式如下

src/controllers/post.js
const createDraft = async (req, res) => {
const { title, content, authorEmail } = req.body

try {
const author = await User.findOne({ email: authorEmail })

if (!author) return res.status(404).json({ message: 'Author not found' })

const draft = await Post.create({
title,
content,
author: author._id,
})

res.status(201).json(draft)
} catch (error) {
return res.status(500).json(error)
}
}

使用 Prisma ORM,路由处理程序的实现方式如下

src/controllers/post.js
const createDraft = async (req, res) => {
const { title, content, authorEmail } = req.body

try {
const draft = await prisma.post.create({
data: {
title,
content,
author: {
connect: {
email: authorEmail,
},
},
},
})

res.status(201).json(draft)
} catch (error) {
return res.status(500).json(error)
}
}

请注意,Prisma Client 嵌套写入在这里保存了最初的查询,其中 User 记录首先通过其 email 检索。这是因为,使用 Prisma Client,您可以使用任何唯一属性在关系中连接记录。

/user/:id/profile

/user/:id/profile 处理程序的实现方式如下

src/controllers/user.js
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body

try {
const user = await User.findByIdAndUpdate(
id,
{
profile: {
bio,
},
},
{ new: true }
)

if (!user) return res.status(404).json({ message: 'Author not found' })

return res.status(200).json(user)
} catch (error) {
return res.status(500).json(error)
}
}

使用 Prisma ORM,路由处理程序的实现方式如下

src/controllers/user.js
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body

try {
const user = await prisma.user.update({
where: { id },
data: {
profile: {
bio,
},
},
})

if (!user) return res.status(404).json({ message: 'Author not found' })

return res.status(200).json(user)
} catch (error) {
console.log(error)
return res.status(500).json(error)
}
}

或者,您可以使用 set 属性以如下方式更新嵌入式文档的值

src/controllers/user.js
const setUserBio = async (req, res) => {
const { id } = req.params
const { bio } = req.body

try {
const user = await prisma.user.update({
where: {
id,
},
data: {
profile: {
set: { bio },
},
},
})

return res.status(200).json(user)
} catch (error) {
console.log(error)
return res.status(500).json(error)
}
}

4.3. 替换 PUT 请求中的查询

REST API 有两个接受 PUT 请求的路由

  • /post/:id/:categoryId:将具有 :id 的帖子添加到具有 :categoryId 的类别中
  • /post/:id:将帖子的 published 状态更新为 true。

让我们深入研究实现这些请求的路由处理程序。

/post/:id/:categoryId

/post/:id/:categoryId 处理程序的实现方式如下

src/controllers/post.js
const addPostToCategory = async (req, res) => {
const { id, categoryId } = req.params

try {
const category = await Category.findById(categoryId)

if (!category)
return res.status(404).json({ message: 'Category not found' })

const post = await Post.findByIdAndUpdate(
{ _id: id },
{
categories: [{ _id: categoryId }],
},
{ new: true }
)

if (!post) return res.status(404).json({ message: 'Post not found' })
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}

使用 Prisma ORM,处理程序的实现方式如下

src/controllers/post.js
const addPostToCategory = async (req, res) => {
const { id, categoryId } = req.query

try {
const post = await prisma.post.update({
where: {
id,
},
data: {
categories: {
connect: {
id: categoryId,
},
},
},
})

if (!post) return res.status(404).json({ message: 'Post not found' })

return res.status(200).json(post)
} catch (error) {
console.log({ error })
return res.status(500).json(error)
}
}

/post/:id

/post/:id 处理程序的实现方式如下

src/controllers/post.js
const publishDraft = async (req, res) => {
const { id } = req.params

try {
const post = await Post.findByIdAndUpdate(
{ id },
{ published: true },
{ new: true }
)
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}

使用 Prisma ORM,处理程序的实现方式如下

src/controllers/post.js
const publishDraft = async (req, res) => {
const { id } = req.params

try {
const post = await prisma.post.update({
where: { id },
data: { published: true },
})
return res.status(200).json(post)
} catch (error) {
return res.status(500).json(error)
}
}

更多

嵌入式文档 _id 字段

默认情况下,Mongoose 会为每个文档和嵌入式文档分配一个 _id 字段。如果您希望为嵌入式文档禁用此选项,可以将 _id 选项设置为 false。

const ProfileSchema = new Schema(
{
bio: String,
},
{
_id: false,
}
)

文档版本键

Mongoose 会在创建时为每个文档分配一个版本。您可以通过将模型的 versionKey 选项设置为 false 来禁用 Mongoose 对文档进行版本控制。除非您是高级用户,否则 不建议 禁用它。

const ProfileSchema = new Schema(
{
bio: String,
},
{
versionKey: false,
}
)

迁移到 Prisma ORM 时,将 versionKey 字段标记为可选 ( ? ) 在您的 Prisma 模式中,并添加 @ignore 属性以将其从 Prisma Client 中排除。

集合名称推断

Mongoose 会通过自动将模型名称转换为小写和复数形式来推断集合名称。

另一方面,Prisma ORM 会将模型名称映射到数据库中的表名称 建模您的数据

您可以强制 Mongoose 中的集合名称与模型名称相同,方法是在创建模式时设置 选项

const PostSchema = new Schema(
{
title: String,
content: String,
// more fields here
},
{
collection: 'Post',
}
)

建模关系

您可以使用 Mongoose 在文档之间建模关系,方法是使用 子文档 或存储对其他文档的 引用

Prisma ORM 允许您在使用 MongoDB 时对文档之间的不同类型的关系进行建模