跳至主要内容

从 Mongoose 迁移

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

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

迁移过程概述

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

  1. 安装 Prisma CLI
  2. 内省您的数据库
  3. 安装并生成 Prisma Client
  4. 逐步用 Prisma Client 替换您的 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 Client API 的基础,它允许您向您的数据库发送查询。

2.4. 更新关系

MongoDB 不支持不同集合之间的关系。但是,您可以使用 ObjectId 字段类型或从一个文档到多个文档使用集合中的 ObjectId 数组来创建文档之间的引用。该引用将存储相关文档的 id。您可以使用 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 关系
  • 在两个模型上都添加一个 关系标量

  • 在两侧添加指定 fieldsreferences 参数的 @relation 属性
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 以指示它是复数形式。

更新 published 字段,包括 @default 属性以定义字段的默认值。

您还可以将 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. 使用 Prisma Client 替换您的 Mongoose 查询

在本节中,我们将展示一些从 Mongoose 迁移到 Prisma Client 的示例查询,这些查询基于示例 REST API 项目中的示例路由。有关 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 时,在您的 Prisma 模式中将 versionKey 字段标记为可选( ? ),并添加 @ignore 属性以将其从 Prisma Client 中排除。

集合名称推断

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

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

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

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

建模关系

您可以通过使用子文档或存储对其他文档的引用在 Mongoose 中对文档之间的关系进行建模。

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