NestJSでPrismaを操作する
Prismaは現在、主に以下のデータベースをサポートしています。
- PostgreSQL
- MySQL
- SQL Server
- SQLite
- MongoDB
PrismaはJavaScriptでも活用できます。しかし、本章ではPrismaの型安全性を最大限に活用するためにTypeScriptを使います。競合のTypeORMとの詳細な性能の比較はこちらの記事で確認できます。
メモ
Prismaの動作をざっくり理解したければ、Quickstartに従うか、公式ドキュメントのはじめにを参照してください。また、公式GitHubの
prisma-examples
リポジトリにはREST APIとGraphQL API両方のサンプルが用意されています。あと、ざっくりでも概要を確認したい人(忙しい方)はこちらの記事を参照してください。(英語ですが)
正式名称からわかるように、オブジェクトとデータベースのマッピングを行うもの です。
多くのプログラミング言語はオブジェクトを扱うので、そのオブジェクトをデータベースに保存できるように、対応付けを簡単にするためにORMを活用します。より簡単に言えば、SQLを直接書かずにオブジェクトのメソッドだけでデータベースを操作できるということになります。
また、データベースの作成やマイグレーションなどの操作もORMで実施できます。
本章では、NestJSとPrismaをゼロから始める方法について学習します。データベースのデータを読み書きできるREST APIを持つサンプルNestJSアプリケーションを開発していきます。
本ガイドでは、データベースサーバのセットアップのオーバーヘッドを節約するためにSQLiteを使います。PostgreSQLやMySQLを使用している場合でも、本ガイドに従うことができます。適切な場所に、これらのデータベースを使用する ための特別な説明があります。
メモ
すでに既存のプロジェクトを持っていて、Prismaへの移行を検討している場合は「既存のプロジェクトにPrismaを追加するためのガイド」を参照ください。TypeORMから移行する場合は、「TypeORMからPrismaへ移行するためのガイド」を参照ください。
まずはNestJS CLIをインストールして以下のコマンドを実行します。
npm install -g @nestjs/cli
npx nest new hello-prisma
http://localhost:3000/
で動作する REST API は、現在src/app.controller.ts
に実装されている 1 つのルートを提供しています。このガイドの過程で、ユーザーや投稿に関するデータを保存・取得するためのルートを追加で実装していきます。まずはPrisma CLIを開発用依存ファイルとしてプロジェクトにインストールします。
cd hello-prisma
npm install prisma --save-dev
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
と呼ばれる)を指定するだけでよいのです。このファイルは次のステップで作成されます。以下のようにファイルを調整します。
すべて大文字で綴られたプレースホルダーをデータベース認証情報に置き換えます。
SCHEMA
プレースホルダーに何を指定すればいいのかわからない場合は、デフォルトの値であるpublic
を指定することが多いので注意しましょう。DATABASE_URL="postgresql://USER:[email protected]:PORT/DATABASE?schema=public"
以下のようにファイルを調整します。
大文字で書かれたプレースホルダーを、あなたのデータベース認証情報に置き換えましょう。
このセクションでは、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のモデル定義から生成される型安全のデータベースクライアントです。このアプローチにより、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 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,
});
}
}
UserService
とPostService
は現在、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) });
}
}
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()
このページでは、REST APIを実装するために、NestJSとともにPrismaを使用する方法を学びました。APIルートを実装するコントローラは、
PrismaService
を呼び出し、Prisma Clientを使用してデータベースにクエリを送信し、送られてくるリクエストのデータニーズを満たすようにします。NestJSとPrismaの併用についてもっと知りたい方は、以下のリソースをぜひご覧ください。
Last modified 6mo ago