メインコンテンツへスキップ

Mongoose からの移行

このガイドでは、Mongoose から Prisma ORM への移行方法について説明します。このガイドでは、Mongoose Express のサンプルを拡張したものをサンプルプロジェクトとして使用し、移行手順を説明します。このガイドで使用しているサンプルはGitHubで確認できます。

Prisma ORM と Mongoose の比較については、「Prisma ORM vs Mongoose」ページでご確認ください。

移行プロセスの概要

Mongoose から Prisma ORM への移行手順は、構築しているアプリケーションや API レイヤの種類に関係なく常に同じであることに注意してください。

  1. Prisma CLI をインストールする
  2. データベースをイントロスペクトする
  3. Prisma Client をインストールして生成する
  4. 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 つのサブドキュメント(埋め込みドキュメント)があります。

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

このコマンドでは、以下が作成されます。

  • schema.prisma ファイルを含む 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 フィールド型を使用するか、コレクション内の ObjectIds の配列を使用して、ドキュメント間の参照を作成できます。参照には、関連するドキュメントの ID が保存されます。Mongoose が提供する populate() メソッドを使用して、参照に関連ドキュメントのデータを入力できます。

Post <-> User 間の 1 対 n の関係を次のように更新します。

  • posts モデルの既存の author 参照を authorId に名前変更し、@map("author") 属性を追加します。
  • posts モデルに author 関係フィールドを追加し、fieldsreferences を指定する @relation 属性を追加します。
  • 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 関係を追加します。
  • 両方のモデルに関係スカラーを追加します。
  • 両側で fields 引数と references 引数を指定する @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 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スキーマのバージョンを以下に示します。

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クライアントのインストール

次のステップとして、プロジェクトに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をインスタンス化し、後でルートハンドラーで使用できるように、ファイルからエクスポートします。

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リクエストを受け入れる4つのルートがあります。

  • /feed?searchString={searchString}&take={take}&skip={skip}:公開済みのすべての投稿を返します。
    • クエリパラメーター(オプション)
      • searchStringtitleまたはcontentで投稿をフィルタリングします。
      • 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クライアントを使用して実装する方法を次に示します。

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クライアントが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リクエストを受け入れる3つのルートがあります。

  • /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クライアントのネストされた書き込みは、Userレコードが最初にemailで取得される最初のクエリを保存することに注意してください。これは、Prismaクライアントでは、リレーションでレコードを任意のユニークなプロパティを使用して接続できるためです。

/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リクエストを受け入れる2つのルートがあります。

  • /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クライアントから除外します。

コレクション名の推論

Mongooseは、モデル名を自動的に小文字で複数形に変換することで、コレクション名を推論します。

一方、Prisma ORMは、モデル名をデータベースのテーブル名にマッピングします。データのモデリングを参照してください。

Mongooseでは、スキーマ作成時にオプションを設定することで、モデル名と同じコレクション名を強制的に使用できます。

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

リレーションのモデリング

Mongooseでは、サブドキュメントを使用するか、他のドキュメントへの参照を保存することで、ドキュメント間のリレーションをモデル化できます。

Prisma ORMでは、MongoDBを使用する際に、ドキュメント間のさまざまなタイプのリレーションをモデル化できます。