【NestJS × Prisma × mongo】dockerで起動したmongoにPrisma + NestJSで接続する
TypeScriptの学習を兼ねてNestJSで簡単なCRUD操作するAPIを作成してみたので、その備忘録です。Prisma × mongoの組み合わせの情報があまりなく結構ハマったので誰かの役に立てれば幸いです!(筆者はTypeScript, NestJSは初学者のため誤った箇所があればコメントください!!)
Hello World
以下コマンドで雛形を作成。パッケージングマネージャーを聞かれるのでよしなに。
npx nest new <プロジェクト名>
完了したら既に用意されているエンドポイントにcurlして動作確認。Hello Worldが返って来ればおけ。
npm run start
curl http://localhost:3000
> Hello World!!
Prisma
以下、コマンドで準備。
npm install prisma --save-dev
npx prisma init
完了したら/prisma/schema.prismaと.envファイルが作成されているはず。初期設定ではpostgresqlが記載されているのでmongodbに変更し、適当にコレクション情報を追記する。.envファイルもmongodbに変更しておく。
generator client {
provider = "prisma-client-js"
}
datasource db {
- provider = "postgresql"
+ provider = "mongodb"
url = env("DATABASE_URL")
}
+ model User {
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ name String
+ age Int
+ comment String?
+}
+
+ model UserFriendRelation {
+ id String @id @default(auto()) @map("_id") @db.ObjectId
+ userId String @db.ObjectId
+ friendId String @db.ObjectId
+ createdAt DateTime
+}
- DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
+ DATABASE_URL="mongodb://localhost:27017/master"
変更が完了したら@prisma/clientをインストールしschema.prismaからコレクション情報をマッピングした型情報を生成します。
npm install @prisma/client
インストールが完了したら、以下のようなPrismaServiceを作成します。
import { OnModuleInit } from '@nestjs/common';
import { INestApplication } from '@nestjs/common';
import { Injectable } 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();
});
}
}
main.tsに下記を追記。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PrismaService } from './prisma.service';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
+ const prismaService = app.get(PrismaService);
+ await prismaService.enableShutdownHooks(app);
}
bootstrap();
prismaの準備ができたので既に用意されているapp.service.ts, app.controller.tsに追記していく。
@Injectable()
export class AppService {
+ constructor(private readonly prismaService: PrismaService) {}
getHello(): string {
return 'Hello World!';
}
+ async save(data: Prisma.UserCreateInput): Promise<User> {
+ return this.prismaService.user.create({ data });
+ }
+
+ async findById(
+ userWhereUniqueInput: Prisma.UserWhereUniqueInput,
+ ): Promise<User | null> {
+ return this.prismaService.user.findUnique({ where: userWhereUniqueInput });
+ }
}
+ type GetUserInput = {
+ readonly id: string;
+};
+type CreateUserInput = Readonly<Omit<Prisma.UserCreateInput, 'id'>>;
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
+ @Get('/user/:id')
+ async getUser(@Param() params: GetUserInput): Promise<User> {
+ return await this.appService.findById(params);
+ }
+
+ @Post('/user')
+ async createUser(@Body() input: CreateUserInput): Promise<User> {
+ return await this.appService.save(input);
+ }
}
DB準備
今回はmongoを採用。ローカルで構築しても問題ないけどdockerで構築します。一旦、以下のようなdocker-compose.ymlを作成して起動。(ここから結構ハマります)
version: "3.1"
services:
mongo:
image: mongo
container_name: mongo
restart: always
ports:
- "27017:27017"
volumes:
- ./data/mongo:/data/db
networks:
- app-net
mongo-express:
image: mongo-express
restart: always
ports:
- "8081:8081"
depends_on:
- mongo
environment:
ME_CONFIG_MONGODB_URL: mongodb://@mongo:27017/
ME_CONFIG_MONGODB_SERVER: mongo
networks:
- app-net
networks:
app-net:
driver: bridge
dockerとアプリ起動。
docker-compose up -d
npm run start
試しにcurlでユーザーを作成しようとしましたがエラー。
curl -X POST -H "Content-Type: application/json" http://localhost:3000/user/ -d '{"name": "user1", "age": 32}'
Prisma needs to perform transactions, which requires your MongoDB server to be run as a replica set.
とあるのですが、これはPrismaの内部的な話でトランザクション貼るように動いていて、mongoでトランザクションを貼るにはレプリカセットの構成が必要だけどレプリカセットになっていないからダメよと怒られている。
ので、mongoをレプリカセットで動かす必要がある。これは公式にも書いてあってAtlasとか使えば楽だから頑張ってねみたいなことが書いてあるけどdockerでやりたいのでググりまくる。
これは、結果うまくいかなかったので折り畳んでます。(検証しきれていないので参考程度に興味がある方だけ見ていただければと思います。)
コンテナ複数起動してレプリケーションを設定してみる
docker-compose.ymlを修正して、コンテナを3台構成にしてレプリケーションを設定してみる。
version: "3.1"
services:
mongo-primary:
image: mongo
command: mongod --replSet replset --auth --keyFile /etc/mongod-keyfile
container_name: mongo
restart: always
ports:
- "27017:27017"
volumes:
- ./data/mongo:/data/db
# 初期化スクリプトの配置
- ./etc/init/:/docker-entrypoint-initdb.d/init.js:ro
- ./etc/mongod-keyfile:/etc/mongod-keyfile:ro
networks:
- app-net
mongo-secondary:
image: mongo
command: mongod --replSet replset --auth --keyFile /etc/mongod-keyfile
container_name: mongo
restart: always
ports:
- "27017:27017"
volumes:
- ./data/mongo:/data/db
- ./etc/mongod-keyfile:/etc/mongod-keyfile:ro
networks:
- app-net
mongo-arbiter:
image: mongo
command: mongod --replSet replset --auth --keyFile /etc/mongod-keyfile
container_name: mongo
restart: always
ports:
- "27017:27017"
volumes:
- ./etc/mongod-keyfile:/etc/mongod-keyfile:ro
networks:
- app-net
mongo-express:
image: mongo-express
restart: always
ports:
- "8081:8081"
depends_on:
- mongo
environment:
ME_CONFIG_MONGODB_URL: mongodb://@mongo:27017/
ME_CONFIG_MONGODB_SERVER: mongo
networks:
- app-net
networks:
app-net:
driver: bridge
レプリケーションで認証する場合、各サーバーに認証鍵を作成し、適切なパーミッションを割り当て、配置する。パーミッションが適切でないとエラーになる。
また、レプリカセットの初期化のために以下のようなスクリプトを用意し、配置する。
rs.initiate({
_id: "replset",
members: [{
_id: 0,
host: "mongodb-primary:27017"
}, {
_id: 1,
host: "mongodb-secondary:27017"
}, {
_id: 2,
host: "mongodb-arbiter:27017",
arbiterOnly: true
}]
});
ここまでやってうまく動かなかったのでこの方法は断念。
上の方法でうまくいかなかったので再度色々検索し、Prismaに同じ様な議論をしているisuueがあったので読んでみる。
ちゃんとした内容はリンクを見ていただければと思いますが、「Prismaでmongo使おうとするとレプリケーションの設定が求められるけど開発の環境でそこまでやるのは大変だからオプション作った方が良くない? → ローカルでレプリケーション設定するの簡単だからそこまでしなくていんじゃない?例えばこんな感じ」みたいなことを話している。
version: '3'
services:
mongo:
container_name: mongo
image: mongo:4
command: --replSet rs0
ports:
- '27017:27017'
- '28017:28017'
volumes:
- ./data/mongo:/data/db
一旦、これをふまえてdocker-compose.ymlを修正してみる。
version: "3.1"
services:
mongo:
image: mongo
+ command: --replSet rs0
container_name: mongo
restart: always
ports:
- "27017:27017"
+ - "28017:28017"
volumes:
- ./data/mongo:/data/db
networks:
- app-net
mongo-express:
image: mongo-express
restart: always
ports:
- "8081:8081"
depends_on:
- mongo
environment:
ME_CONFIG_MONGODB_URL: mongodb://@mongo:27017/
ME_CONFIG_MONGODB_SERVER: mongo
networks:
- app-net
networks:
app-net:
driver: bridge
動いた!!!
NAME COMMAND SERVICE STATUS PORTS
demo-app-mongo-express-1 "tini -- /docker-ent…" mongo-express running 0.0.0.0:8081->8081/tcp
mongo "docker-entrypoint.s…" mongo running 0.0.0.0:27017->27017/tcp, 0.0.0.0:28017->28017/tcp
動作確認
dockerとNestアプリが起動していることを確認し再度curlしてみます。
curl -X POST -H "Content-Type: application/json" http://localhost:3000/user/ -d '{"name": "user1", "age": 32}'
> {"id":"62b2743661de9cfd50ae78a5","name":"user1","age":32,"comment":null}
ユーザーが作成できました。次に、レスポンスのidを使ってGetしてみます。
curl http://localhost:3000/user/62b2743661de9cfd50ae78a5
> {"id":"62b2743661de9cfd50ae78a5","name":"user1","age":32,"comment":null}
取得できました!
まとめ
Prismaとmongoの組み合わせをするときはmongoにレプリケーションの設定をしないといけない。dockerでmongoのレプリケーション設定をするのは意外と簡単だがハマりどころも多い。NestJS楽しい。
Discussion