NestJSでPrismaを操作する

Prismaとは

PrismaとはNode.jsとTypeScript専用のオープンソースのORMです。knex.jsなどのSQLクエリビルダやTypeORMなどのデータベースアクセスツールの代替として使用されます。
Prismaは現在、主に以下のデータベースをサポートしています。
  • PostgreSQL
  • MySQL
  • SQL Server
  • SQLite
  • MongoDB
PrismaはJavaScriptでも活用できます。しかし、本章ではPrismaの型安全性を最大限に活用するためにTypeScriptを使います。競合のTypeORMとの詳細な性能の比較はこちらの記事で確認できます。
メモ
Prismaの動作をざっくり理解したければ、Quickstartに従うか、公式ドキュメントのはじめにを参照してください。また、公式GitHubのprisma-examplesリポジトリにはREST APIGraphQL API両方のサンプルが用意されています。
あと、ざっくりでも概要を確認したい人(忙しい方)はこちらの記事を参照してください。(英語ですが)

ORM(Object-Relational Mapping)とは

正式名称からわかるように、オブジェクトとデータベースのマッピングを行うものです。
多くのプログラミング言語はオブジェクトを扱うので、そのオブジェクトをデータベースに保存できるように、対応付けを簡単にするためにORMを活用します。より簡単に言えば、SQLを直接書かずにオブジェクトのメソッドだけでデータベースを操作できるということになります。
また、データベースの作成やマイグレーションなどの操作もORMで実施できます。

はじめに

本章では、NestJSとPrismaをゼロから始める方法について学習します。データベースのデータを読み書きできるREST APIを持つサンプルNestJSアプリケーションを開発していきます。
本ガイドでは、データベースサーバのセットアップのオーバーヘッドを節約するためにSQLiteを使います。PostgreSQLやMySQLを使用している場合でも、本ガイドに従うことができます。適切な場所に、これらのデータベースを使用するための特別な説明があります。
メモ
すでに既存のプロジェクトを持っていて、Prismaへの移行を検討している場合は「既存のプロジェクトにPrismaを追加するためのガイド」を参照ください。TypeORMから移行する場合は、「TypeORMからPrismaへ移行するためのガイド」を参照ください。

NestJSプロジェクトのはじめかた

まずはNestJS CLIをインストールして以下のコマンドを実行します。
npm install -g @nestjs/cli
npx nest new hello-prisma
このコマンドで作成されるプロジェクトファイルの詳細については、開発環境の準備のページを参照ください。また、npm startを実行してアプリケーションを実行できることにも十分に注意してください。
http://localhost:3000/で動作する REST API は、現在src/app.controller.tsに実装されている 1 つのルートを提供しています。このガイドの過程で、ユーザーや投稿に関するデータを保存・取得するためのルートを追加で実装していきます。

Prismaのセットアップ

まずはPrisma CLIを開発用依存ファイルとしてプロジェクトにインストールします。
cd hello-prisma
npm install prisma --save-dev
以下の手順では、Prisma CLIを使います。npx経由でローカルにCLIを起動しましょう。
npx prisma
# yarn経由
yarn add prisma --dev
yarn prisma
ここで、Prisma CLIのinitコマンドを使ってPrismaの初期設定を行います。
npx prisma init
このコマンドは、以下のファイルで新しいprismaディレクトリを作成します。
  • schema.prisma:データベース接続を指定し、データベーススキーマを格納します。
  • .env:データベースの認証情報を環境変数のグループに格納する際に使います。

データベースへの接続

データベース接続はschema.prismaファイルのdatasourceブロックで設定されます。デフォルトではpostgresqlに設定されていますが、このガイドではSQLiteを使用するので、データソースブロックのプロバイダフィールドをsqliteに調整する必要があります。
ここで、.envを開いてDATABASE_URL環境変数を以下のように調整します。
.env
DATABASE_URL="file:./dev.db"
SQLiteは単純なファイルであり、SQLiteを使用するためにサーバーは必要ありません。したがって、ホストとポートを含む接続URLを設定する代わりに、ローカルファイル(この場合dev.dbと呼ばれる)を指定するだけでよいのです。このファイルは次のステップで作成されます。

PostgreSQLあるいはMySQLを使う場合

PostgreSQLとMySQLでは、データベースサーバーを指し示す接続URLを設定する必要があります。必要な接続URLの形式については、こちらの記事で詳しく説明しています。

PostgreSQL

以下のようにファイルを調整します。
.env
DATABASE_URL="postgresql://USER:[email protected]:PORT/DATABASE?schema=SCHEMA"
すべて大文字で綴られたプレースホルダーをデータベース認証情報に置き換えます。SCHEMAプレースホルダーに何を指定すればいいのかわからない場合は、デフォルトの値であるpublicを指定することが多いので注意しましょう。
DATABASE_URL="postgresql://USER:[email protected]:PORT/DATABASE?schema=public"
PostgreSQLのセットアップを学びたい場合は、こちらのガイドに従ってHerokuに無料のPostgreSQLデータベースをセットアップできます。

MySQL

以下のようにファイルを調整します。
.env
DATABASE_URL="mysql://USER:[email protected]:PORT/DATABASE"
大文字で書かれたプレースホルダーを、あなたのデータベース認証情報に置き換えましょう。

データベースをPrismaで作成する

このセクションでは、Prisma Migrateを使用して、データベースに2つの新しいテーブルを作成します。Prisma Migrateは、Prismaスキーマの宣言型データモデル定義に対応したSQLマイグレーションファイルを生成します。これらの移行ファイルは完全にカスタマイズ可能で、基礎となるデータベースの追加機能を構成したり、シードなどの追加コマンドを含めることができます。
schema.prismaファイルに次の 2 つのモデルを追加します。
Prismaモデルを配置した状態で、SQLの移行ファイルを生成し、データベースに対して実行することができます。ターミナルで以下のコマンドを実行します。
npx prisma migrate dev --name init
このprisma migrate devコマンドは、SQL ファイルを生成し、データベースに対して直接実行します。今回は、既存のprismaディレクトリに以下のようなマイグレーションファイルを作成しました。
$ tree prisma
prisma
├── dev.db
├── migrations
│ └── 20201207100915_init
│ └── migration.sql
└── schema.prisma
SQLiteに以下のデータベースが作成されます。
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"name" TEXT
);
-- CreateTable
CREATE TABLE "Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN DEFAULT false,
"authorId" INTEGER,
FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");

Prisma Clientのインストールと生成

Prisma Clientは、Prismaのモデル定義から生成される型安全のデータベースクライアントです。このアプローチにより、Prisma Clientはモデルに特化したCRUD操作を公開することができます。
プロジェクトにPrisma Clientをインストールするには、ターミナルで次のコマンドを実行します。
npm install @prisma/client
インストール時に、Prismaは自動的にprisma generateコマンドを呼び出すことに注意してください。今後、Prismaモデルを変更するたびにこのコマンドを実行し、生成されたPrismaクライアントを更新する必要があります。
メモ
prisma generateコマンドはPrismaスキーマを読み込み、node_modules/@prisma/client内に生成されたPrismaクライアントライブラリを更新します。

NestJSアプリ内でPrisma Clientを使う

これで、Prisma Clientでデータベースクエリを送信できるようになりました。Prisma Clientでのクエリ作成について詳しく知りたい方は、APIドキュメントをご覧ください。
NestJSアプリケーションをセットアップする際、サービス内のデータベースクエリのために、Prisma Client APIを抽象化したいと思います。まず、PrismaClientのインスタンス化とデータベースへの接続を行う新しいPrismaServiceを作成します。
srcディレクトリ内にprisma.service.tsというファイルを新規に作成し、以下のコードを追加してください。
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}
メモ
onModuleInitはオプションで、これを省略すると、Prismaは最初のデータベースへの呼び出し時に遅延的に接続します。Prismaには独自のシャットダウンフックがあり、そこで接続を破棄するため、onModuleDestroyを気にする必要はありません。enableShutdownHooksの詳細については、enableShutdownHooksの問題点を参照してください。
次に、PrismaスキーマからUserモデルとPostモデルのデータベースを呼び出すために使用できるサービスを記述することができます。
srcディレクトリの中にuser.service.tsというファイルを新規に作成し、以下のコードを追加してください。
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { User, Prisma } from '@prisma/client';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
): Promise<User | null> {
return this.prisma.user.findUnique({
where: userWhereUniqueInput,
});
}
async users(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.user.create({
data,
});
}
async updateUser(params: {
where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput;
}): Promise<User> {
const { where, data } = params;
return this.prisma.user.update({
data,
where,
});
}
async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
return this.prisma.user.delete({
where,
});
}
}
Prisma Clientの生成された型を使用して、サービスによって公開されるメソッドが適切に型付けされていることを確認していることに注意してください。したがって、モデルをタイプし、追加のインタフェースまたはDTOファイルを作成するという煩雑な作業を省くことができます。
今度はPostモデルに対して同じことを行ってください。
srcディレクトリの中にpost.service.tsというファイルを新規に作成し、以下のコードを追加してください。
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Post, Prisma } from '@prisma/client';
@Injectable()
export class PostService {
constructor(private prisma: PrismaService) {}
async post(
postWhereUniqueInput: Prisma.PostWhereUniqueInput,
): Promise<Post | null> {
return this.prisma.post.findUnique({
where: postWhereUniqueInput,
});
}
async posts(params: {
skip?: number;
take?: number;
cursor?: Prisma.PostWhereUniqueInput;
where?: Prisma.PostWhereInput;
orderBy?: Prisma.PostOrderByWithRelationInput;
}): Promise<Post[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.post.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async createPost(data: Prisma.PostCreateInput): Promise<Post> {
return this.prisma.post.create({
data,
});
}
async updatePost(params: {
where: Prisma.PostWhereUniqueInput;
data: Prisma.PostUpdateInput;
}): Promise<Post> {
const { data, where } = params;
return this.prisma.post.update({
data,
where,
});
}
async deletePost(where: Prisma.PostWhereUniqueInput): Promise<Post> {
return this.prisma.post.delete({
where,
});
}
}
UserServicePostServiceは現在、Prisma Clientで利用可能なCRUDクエリをラップしています。実際のアプリケーションでは、このサービスはアプリケーションにビジネスロジックを追加する場所にもなります。例えば、UserService内にupdatePasswordというメソッドを用意して、ユーザーのパスワードの更新を担当させることができます。
メインアプリコントローラにREST APIルートを実装する 最後に、前のセクションで作成したサービスを使用して、アプリのさまざまな経路を実装します。このガイドでは、すべてのルートを既に存在する AppControllerクラスに実装することにします。
app.controller.tsファイルの内容を、次のコードに置き換えてください。
import {
Controller,
Get,
Param,
Post,
Body,
Put,
Delete,
} from '@nestjs/common';
import { UserService } from './user.service';
import { PostService } from './post.service';
import { User as UserModel, Post as PostModel } from '@prisma/client';
@Controller()
export class AppController {
constructor(
private readonly userService: UserService,
private readonly postService: PostService,
) {}
@Get('post/:id')
async getPostById(@Param('id') id: string): Promise<PostModel> {
return this.postService.post({ id: Number(id) });
}
@Get('feed')
async getPublishedPosts(): Promise<PostModel[]> {
return this.postService.posts({
where: { published: true },
});
}
@Get('filtered-posts/:searchString')
async getFilteredPosts(
@Param('searchString') searchString: string,
): Promise<PostModel[]> {
return this.postService.posts({
where: {
OR: [
{
title: { contains: searchString },
},
{
content: { contains: searchString },
},
],
},
});
}
@Post('post')
async createDraft(
@Body() postData: { title: string; content?: string; authorEmail: string },
): Promise<PostModel> {
const { title, content, authorEmail } = postData;
return this.postService.createPost({
title,
content,
author: {
connect: { email: authorEmail },
},
});
}
@Post('user')
async signupUser(
@Body() userData: { name?: string; email: string },
): Promise<UserModel> {
return this.userService.createUser(userData);
}
@Put('publish/:id')
async publishPost(@Param('id') id: string): Promise<PostModel> {
return this.postService.updatePost({
where: { id: Number(id) },
data: { published: true },
});
}
@Delete('post/:id')
async deletePost(@Param('id') id: string): Promise<PostModel> {
return this.postService.deletePost({ id: Number(id) });
}
}

enableShutdownHooksの問題点

PrismaはNestJSのenableShutdownHooksと干渉します。Prismaはシャットダウンシグナルをリッスンし、アプリケーションのシャットダウンフックが起動する前にprocess.exit()を呼び出します。この問題に対処するためには、PrismaのbeforeExitイベントに対するリスナーを追加する必要があります。
main.ts
...
import { PrismaService } from './services/prisma/prisma.service';
...
async function bootstrap() {
...
const prismaService = app.get(PrismaService);
await prismaService.enableShutdownHooks(app)
...
}
bootstrap()
Prismaのシャットダウン信号の処理、およびbeforeExitイベントについては、こちらをご覧ください。

要約

このページでは、REST APIを実装するために、NestJSとともにPrismaを使用する方法を学びました。APIルートを実装するコントローラは、PrismaServiceを呼び出し、Prisma Clientを使用してデータベースにクエリを送信し、送られてくるリクエストのデータニーズを満たすようにします。
NestJSとPrismaの併用についてもっと知りたい方は、以下のリソースをぜひご覧ください。