Mongoose からの移行
このガイドでは、Mongoose から Prisma ORM への移行方法について説明します。このガイドでは、Mongoose Express のサンプルを拡張したものをサンプルプロジェクトとして使用し、移行手順を説明します。このガイドで使用しているサンプルはGitHubで確認できます。
Prisma ORM と Mongoose の比較については、「Prisma ORM vs Mongoose」ページでご確認ください。
移行プロセスの概要
Mongoose から Prisma ORM への移行手順は、構築しているアプリケーションや API レイヤの種類に関係なく常に同じであることに注意してください。
- Prisma CLI をインストールする
- データベースをイントロスペクトする
- Prisma Client をインストールして生成する
- Mongoose クエリを Prisma Client に徐々に置き換える
これらの手順は、REST API(Express、koa、NestJS など)、GraphQL API(Apollo Server、TypeGraphQL、Nexus など)、または Mongoose をデータベースアクセスに使用するその他の種類のアプリケーションを構築しているかどうかにかかわらず適用されます。
Prisma ORM は、段階的な導入に非常に適しています。つまり、プロジェクト全体を Mongoose から Prisma ORM に一度に移行する必要はなく、データベースクエリを Mongoose から Prisma ORM に段階的に移動できます。
サンプルプロジェクトの概要
このガイドでは、Prisma ORM への移行のサンプルプロジェクトとして、Express で構築された REST API を使用します。これには、3 つのドキュメントと 1 つのサブドキュメント(埋め込みドキュメント)があります。
- post.js
- user.js
- category.js
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)
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ProfileSchema = new Schema(
{
bio: String,
},
{
_id: false,
}
)
const UserSchema = new Schema({
name: String,
email: {
type: String,
unique: true,
},
profile: {
type: ProfileSchema,
default: () => ({}),
},
})
module.exports = mongoose.model('User', UserSchema)
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const CategorySchema = new Schema({
name: {
type: String,
required: true,
},
})
module.exports = mongoose.model('Category', CategorySchema)
モデル/ドキュメントには、次の種類の関係があります。
- 1-n:
User
↔Post
- m-n:
Post
↔Category
- サブドキュメント/埋め込みドキュメント:
User
↔Profile
このガイドで使用されている例では、ルートハンドラーは 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
このコマンドでは、以下が作成されます。
schema.prisma
ファイルを含むprisma
という新しいディレクトリ。Prisma スキーマでは、データベース接続とモデルを指定します。.env
: プロジェクトのルートにある(まだ存在しない場合)、dotenv
ファイル。環境変数としてデータベース接続 URL を構成するために使用されます。
現在の 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 が使用する形式と似ています。
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 モデルが作成されます。
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
フィールド型を使用するか、コレクション内の ObjectIds
の配列を使用して、ドキュメント間の参照を作成できます。参照には、関連するドキュメントの ID が保存されます。Mongoose が提供する populate()
メソッドを使用して、参照に関連ドキュメントのデータを入力できます。
Post
<-> User
間の 1 対 n の関係を次のように更新します。
posts
モデルの既存のauthor
参照をauthorId
に名前変更し、@map("author")
属性を追加します。posts
モデルにauthor
関係フィールドを追加し、fields
とreferences
を指定する@relation
属性を追加します。users
モデルにposts
関係を追加します。
- 差分
- 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
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[]
}
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 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
関係を追加します。- 両方のモデルに関係スカラーを追加します。
- 両側で
fields
引数とreferences
引数を指定する@relation
属性を追加します。
- 差分
- schema.prisma
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[]
}
type UsersProfile {
bio String
}
model categories {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
v Int @map("__v")
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 categories[] @relation(fields: [categoryIds], references: [id])
categoryIds 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[]
}
2.5 Prismaスキーマの調整(オプション)
イントロスペクションによって生成されたモデルは、現在、データベースのコレクションに正確に対応しています。このセクションでは、Prisma ORMの命名規則に従うように、Prismaモデルの名前を調整する方法を学びます。
これらの調整の一部は完全にオプションであり、現時点で何も調整したくない場合は、次のステップにスキップしても構いません。後でいつでも戻って調整を行うことができます。
現在のPrismaモデルのスネークケース表記とは対照的に、Prisma ORMの命名規則は次のとおりです。
- モデル名にはPascalCase
- フィールド名にはcamelCase
Prismaモデルとフィールドの名前を、それぞれ@@map
と@map
を使用して、基盤となるデータベースの既存のテーブルと列名にマッピングすることで、名前を調整できます。
シンボルの名前変更操作を使用して、モデル名をリファクタリングできます。モデル名を強調表示し、F2キーを押し、最後に目的の名前を入力します。これにより、参照されているすべてのインスタンスの名前が変更され、既存のモデルに以前の名前で@@map()
属性が追加されます。
スキーマにversionKey
が含まれている場合は、v
フィールドに@default(0)
および@ignore
属性を追加して更新します。これは、フィールドが生成されたPrismaクライアントから除外され、デフォルト値が0になることを意味します。Prisma ORMは、ドキュメントのバージョン管理を処理しません。
リレーションフィールドの名前を変更して、後でデータベースにクエリを送信するために使用するPrismaクライアントAPIを最適化することもできます。たとえば、user
モデルのpost
フィールドはリストであるため、このフィールドのより良い名前は、複数形であることを示すposts
になります。
published
フィールドを、フィールドのデフォルト値を定義するために@default
属性を含めることで更新します。
UserProfile
複合型をProfile
に名前変更することもできます。
これらを考慮して調整した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クライアントのインストール
次のステップとして、プロジェクトにPrismaクライアントをインストールして、現在Mongooseで作成されているプロジェクト内のデータベースクエリを置き換えることができます。
npm install @prisma/client
ステップ4. MongooseクエリをPrismaクライアントに置き換える
このセクションでは、サンプルREST APIプロジェクトのサンプルルートに基づいて、MongooseからPrismaクライアントに移行されているいくつかのサンプルクエリを示します。PrismaクライアントAPIがMongooseとどのように異なるかの包括的な概要については、MongooseとPrisma APIの比較ページを参照してください。
まず、さまざまなルートハンドラーからデータベースクエリを送信するために使用するPrismaClient
インスタンスを設定するには、src
ディレクトリにprisma.js
という名前の新しいファイルを作成します。
touch src/prisma.js
次に、PrismaClient
をインスタンス化し、後でルートハンドラーで使用できるように、ファイルからエクスポートします。
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
module.exports = prisma
コントローラーファイルのインポートは次のとおりです。
const Post = require('../models/post')
const User = require('../models/user')
const Category = require('../models/category')
const Post = require('../models/post')
const User = require('../models/user')
MongooseからPrismaに移行する際に、コントローラーのインポートを更新します。
const prisma = require('../prisma')
const prisma = require('../prisma')
4.1. GET
リクエストでのクエリの置き換え
このガイドで使用されているサンプルREST APIには、GET
リクエストを受け入れる4つのルートがあります。
/feed?searchString={searchString}&take={take}&skip={skip}
:公開済みのすべての投稿を返します。- クエリパラメーター(オプション)
searchString
:title
またはcontent
で投稿をフィルタリングします。take
:リストで返されるオブジェクトの数を指定します。skip
:返されたオブジェクトのうち、スキップするオブジェクトの数を指定します。
- クエリパラメーター(オプション)
/post/:id
:特定の投稿を返します。/authors
:作成者のリストを返します。
これらのリクエストを実装するルートハンドラーを詳しく見ていきましょう。
/feed
/feed
ハンドラーは次のように実装されています。
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
オブジェクトには、関連付けられているauthor
とcategory
へのリレーションが含まれていることに注意してください。Mongooseでは、リレーションを含めることはタイプセーフではありません。たとえば、取得されるリレーションにタイプミスがあった場合、データベースクエリは実行時にのみ失敗します。JavaScriptコンパイラーはここで安全性を提供しません。
同じルートハンドラーをPrismaクライアントを使用して実装する方法を次に示します。
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クライアントがauthor
リレーションを含める方法は、完全にタイプセーフであることに注意してください。存在しないリレーションをPost
モデルに含めようとすると、JavaScriptコンパイラーはエラーをスローします。
/post/:id
/post/:id
ハンドラーは次のように実装されています。
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を使用すると、ルートハンドラーは次のように実装されます。
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
リクエストを受け入れる3つのルートがあります。
/user
:新しいUser
レコードを作成します。/post
:新しいUser
レコードを作成します。/user/:id/profile
:指定されたIDを持つUser
レコードの新しいProfile
レコードを作成します。
/user
/user
ハンドラーは次のように実装されています。
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を使用すると、ルートハンドラーは次のように実装されます。
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
ハンドラーは次のように実装されています。
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を使用すると、ルートハンドラーは次のように実装されます。
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クライアントのネストされた書き込みは、User
レコードが最初にemail
で取得される最初のクエリを保存することに注意してください。これは、Prismaクライアントでは、リレーションでレコードを任意のユニークなプロパティを使用して接続できるためです。
/user/:id/profile
/user/:id/profile
ハンドラーは次のように実装されています。
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を使用すると、ルートハンドラーは次のように実装されます。
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
プロパティを使用して、次のように埋め込みドキュメントの値を更新できます。
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
リクエストを受け入れる2つのルートがあります。
/post/:id/:categoryId
::id
を持つ投稿を:categoryId
を持つカテゴリーに追加します。/post/:id
:投稿のpublished
ステータスをtrueに更新します。
これらのリクエストを実装するルートハンドラーを詳しく見ていきましょう。
/post/:id/:categoryId
/post/:id/:categoryId
ハンドラーは次のように実装されています。
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を使用すると、ハンドラーは次のように実装されます。
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
ハンドラーは次のように実装されています。
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を使用すると、ハンドラーは次のように実装されます。
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クライアントから除外します。
コレクション名の推論
Mongooseは、モデル名を自動的に小文字で複数形に変換することで、コレクション名を推論します。
一方、Prisma ORMは、モデル名をデータベースのテーブル名にマッピングします。データのモデリングを参照してください。
Mongooseでは、スキーマ作成時にオプションを設定することで、モデル名と同じコレクション名を強制的に使用できます。
const PostSchema = new Schema(
{
title: String,
content: String,
// more fields here
},
{
collection: 'Post',
}
)
リレーションのモデリング
Mongooseでは、サブドキュメントを使用するか、他のドキュメントへの参照を保存することで、ドキュメント間のリレーションをモデル化できます。
Prisma ORMでは、MongoDBを使用する際に、ドキュメント間のさまざまなタイプのリレーションをモデル化できます。