👋

【NestJS × Prisma × mongo】dockerで起動したmongoにPrisma + NestJSで接続する

2022/06/22に公開

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に変更しておく。

schema.prisma
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
+}
.env
- 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を作成します。

prisma.service.ts
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に下記を追記。

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に追記していく。

app.service.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 });
+  }
}
app.controller.ts
+ 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を作成して起動。(ここから結構ハマります)

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台構成にしてレプリケーションを設定してみる。

docker-comose.yml
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

レプリケーションで認証する場合、各サーバーに認証鍵を作成し、適切なパーミッションを割り当て、配置する。パーミッションが適切でないとエラーになる。

また、レプリカセットの初期化のために以下のようなスクリプトを用意し、配置する。

init.js
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があったので読んでみる。

https://github.com/prisma/prisma/issues/8266

ちゃんとした内容はリンクを見ていただければと思いますが、「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を修正してみる。

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楽しい。

GitHubで編集を提案

Discussion