NestJS + Prisma で O/R マッピング

● 1. MySQL の作成 ( Docker )

vi docker-compose.yml
version: "3.8"
services:
  db:
    container_name: myapp-api-db
    image: mysql:8.0
    restart: always
    ports:
      - 13306:3306
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: myapp_api_db
      MYSQL_USER: myapp_api_db
      MYSQL_PASSWORD: db_password
    volumes:
      - ./db/data:/var/lib/mysql

● 2.NestJS + prisma アプリの作成

・nestjs アプリの作成

npm i -g @nestjs/cli
nest new myapp-api

・prismaの インストール

cd myapp-api
npm i @prisma/client
npm i -D prisma

・prismaの 初期化と 設定ファイルの記述

npx prisma init
vi prisma/schema.prisma

prisma/schema.prisma を以下の内容で保存

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

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();
    });
  }
}
vi .env

.env を以下の内容で保存

DATABASE_URL="mysql://myapp_api_db:db_password@localhost:13306/myapp_api_db?schema=public"

● 3.NestJS と mysql の 接続の確認とマイグレーションの実行

次のような簡単なリレーションを持つテーブルを作成します

・prisma/schema.prisma の一番下に テーブル情報を追記します

vi prisma/schema.prisma
model User {
  id          Int           @id @default(autoincrement())
  email       String        @db.Text
  name        String        @db.Text
  createdAt   DateTime      @default(now()) @db.Timestamp(0)
  updatedAt   DateTime      @default(now()) @updatedAt @db.Timestamp(0)
  source      Source[]      @relation("source_fk_1")
  // translation Translation[] @relation("translation_fk_2")
}

model Source {
  id          Int           @id @default(autoincrement())
  userId      Int         
  user        User          @relation(name: "source_fk_1", fields: [userId], references: [id])
  text        String        @db.Text
  translation Translation[] @relation("translation_fk_1")
}

model Translation {
  id          Int           @id @default(autoincrement())
  sourceId    Int
  source      Source        @relation(name: "translation_fk_1", fields: [sourceId], references: [id])
  // userId      Int
  // user        User          @relation(name: "translation_fk_2", fields: [userId], references: [id])
  locale      String        @db.VarChar(8)
  text        String        @db.Text
}

・A. mysqlにマイグレーション実行ユーザを作成(手動でする場合)

docker コンテナ名を確認する

 docker ps

docker コンテナの中に入る

docker exec -it <コンテナ名> bash

mysql ユーザを作成する

mysql -uroot -p mysql
create user translate_api_db@localhost identified by 'db_password';
grant create, alter, drop, references on *.* to translate_api_db;

・B. mysqlにマイグレーション実行ユーザを作成(dockerコンテナ初回実行時に自動でする場合)

以下のようにするとdockerコンテナの初回起動時に ./db/initdb.d/initdb.sql を実行してマイグレーションに必要なユーザーを作成することができます

mkdir db
cd db
mkdir initdb.d
cd initdb.d
vi initdb.sql

initdb.sql を以下の内容で保存します

grant select, insert, update, delete, create, alter, drop, references on *.* to 'myapp_api_db'@'%' with grant option;

docker 起動

docker compose up

うまくいかない場合は次のようなエラーがでているはずです

[Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/init.sql
myapp-api-db  | ERROR 1410 (42000) at line 1: You are not allowed to create a user with GRANT
myapp-api-db exited with code 1

・マイグレーションを実行

npx prisma migrate dev

マイグレーションファイル名入力を促されるので次のように 行った操作を簡単に入力します。(実行日時は自動的にセットされます)

add_tables

実行すると「DBへテーブルが作成され」「実行されたsql文が次のファイル名にて保存され」ます

・ テーブルが作成されたことを確認

MySQLを操作するアプリを起動するか 次のコマンドから確認することもできます

npx prisma studio

http://localhost:5555

マイグレーションファイルを作成せずに、同期する

マイグレーションファイルを生成せず、スキーマを同期する
npx prisma migrate devを実行すると、毎回スキーマファイルが作成されるので、
開発時など頻繁に変更するときには少しめんどくさい。

開発用のコマンドもあり、npx prisma db pushを使うと、
マイグレーションファイルを生成せず同期できる。

いろいろ試して、変更点が整理できたら、
マイグレーションファイルを作成するのがよい感じ。

引用 : https://www.memory-lovers.blog/entry/2021/10/13/113000

● 4. Prisma のDBシーダーを実行してテストデータを投入する

・シーダーファイルの作成

ファイル名 /prisma/seed.ts を作成します。

vi prisma/seed.ts

prisma/seed.ts を 以下の内容で保存します

import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();

const userData: Prisma.UserCreateInput[] = [
  {
    email: 'test@user.com',
    name: 'テスト太郎',
    createdAt: new Date(),
    updatedAt: new Date(),
    source: {
      create: [
        {
          text: 'おはようございます。今日は晴れていますね。',
          translation: {
            create: [
              {
                locale: 'en',
                text: "Good morning. It's a sunny day today.",
              },
              {
                locale: 'pt_BR',
                text: 'Bom dia. Hoje está ensolarado.',
              },
            ],
          },
        },
        {
          text: '昨日の晩御飯は誰と何を食べましたか?',
          translation: {
            create: [
              {
                locale: 'en',
                text: 'Who did you have dinner with and what did you eat yesterday?',
              },
              {
                locale: 'pt_BR',
                text: 'Com quem você jantou e o que comeu ontem à noite?',
              },
            ],
          },
        },
      ],
    },
  },
];

async function main() {
  console.log(`Start seeding ...`);
  for (const u of userData) {
    const user = await prisma.user.create({
      data: u,
    });
    console.log(`Created user with id: ${user.id}`);
  }
  console.log(`Seeding finished.`);
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

create以外にも次のような便利なメソッドがあります
引用 : https://zenn.dev/takky94/scraps/448d1ad68d969a

・データが存在すれば update、存在しなければ create がしたい 
 → Upsert

・あるモデルのcreateやupdate時、そのモデルの関係モデルにデータが存在すればupdate、しなければcreateしたい
 → connectOrCreate

・package.json へ追加

package.json へ次のコードを追記します。 scripts の一番最後の行に入れておくといいでしょう。

  "scripts": {
    ...................
    "seed": "ts-node prisma/seed.ts"
  },

・シーダーを実行する

npm run seed

または 直接ファイルを実行してもokです

npx ts-node prisma/seed.ts 

● 5. prismaを使用してDBからデータ取得する

DBの構成を自動でPrisma Schemaに渡す

graphql のコードも自動生成したい場合は、こちらのパッケージをインストールしておきます
node.js|プログラムメモ

npx prisma db pull
npx prisma generate

(実行すると 型のサポートを受けれるようになります)

.env の代わりに .env.development を 使用している場合は、こちらのコマンドを実行します

npx dotenv -e .env.development --  npx prisma db pull
npx dotenv -e .env.development --  npx prisma generate

サンプルスクリプトを作成する

(ユーザ一覧とその先のリレーション、すべてを取得するスクリプトを記述します)

vi prisma/sample-script.ts

prisma/sample-script.ts

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  const users = await prisma.user.findMany({
    include: {
      source: {
        include: {
          translation: true,
        },
      },
    },
  });
  console.log(JSON.stringify(users, null, 4));
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

include で リレーション先テーブルの すべてのカラムを取得しています

スクリプトを 実行する

npx ts-node prisma/sample-script.ts 

結果

[
    {
        "id": 1,
        "email": "test@user.com",
        "name": "テスト太郎",
        "createdAt": "2023-03-27T04:20:47.000Z",
        "updatedAt": "2023-03-27T04:20:47.000Z",
        "source": [
            {
                "id": 1,
                "userId": 1,
                "text": "おはようございます。今日は晴れていますね。",
                "createdAt": "2023-03-27T04:20:47.000Z",
                "updatedAt": "2023-03-27T04:20:47.000Z",
                "translation": [
                    {
                        "id": 1,
                        "sourceId": 1,
                        "locale": "en",
                        "text": "Good morning. It's a sunny day today.",
                        "createdAt": "2023-03-27T04:20:47.000Z",
                        "updatedAt": "2023-03-27T04:20:47.000Z"
                    },
                    {
                        "id": 2,
                        "sourceId": 1,
                        "locale": "pt_BR",
                        "text": "Bom dia. Hoje está ensolarado.",
                        "createdAt": "2023-03-27T04:20:47.000Z",
                        "updatedAt": "2023-03-27T04:20:47.000Z"
                    }
                ]
            },
            {
                "id": 2,
                "userId": 1,
                "text": "昨日の晩御飯は誰と何を食べましたか?",
                "createdAt": "2023-03-27T04:20:47.000Z",
                "updatedAt": "2023-03-27T04:20:47.000Z",
                "translation": [
                    {
                        "id": 3,
                        "sourceId": 2,
                        "locale": "en",
                        "text": "Who did you have dinner with and what did you eat yesterday?",
                        "createdAt": "2023-03-27T04:20:47.000Z",
                        "updatedAt": "2023-03-27T04:20:47.000Z"
                    },
                    {
                        "id": 4,
                        "sourceId": 2,
                        "locale": "pt_BR",
                        "text": "Com quem você jantou e o que comeu ontem à noite?",
                        "createdAt": "2023-03-27T04:20:47.000Z",
                        "updatedAt": "2023-03-27T04:20:47.000Z"
                    }
                ]
            }
        ]
    }
]

prismaの O/Rマッピングリレーション一覧

https://medium.com/yavar/prisma-relations-2ea20c42f616

● 6. NestJSの REST API エンドポイントの作成

NestJS テストの確認

テストを一通り実行して確認しておきます

npm run test
npm run test:e2e
npm run test:cov

エンドポイント作成の練習

npx nest generate resource users --dry-run

(実際に実行するときは、 --dry-run を外します)

エンドポイント /users 作成

npx nest generate resource users
( REST API  を選択してエンター )

http://localhost:3000/users へアクセス

npm run start:dev

http://localhost:3000/ へ アクセスして Hello,world! が表示されることを確認します
http://localhost:3000/users へ アクセスして This action returns all users が表示されることを確認します

src/users/users.service.ts の修正

以下のように書き換えて保存します

import { Injectable } from '@nestjs/common';
import { CreateGuserInput } from './dto/create-guser.input';
import { PrismaService } from '../prisma.service';

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async findAll() {
    return this.prisma.user.findMany({
      include: {
        source: true,
      },
    });
  }

  async findOne(id: number) {
    return this.prisma.user.findFirst({ where: { id: id } });
  }

  create(createGuserInput: CreateGuserInput) {
    return this.prisma.user.create({
      data: {
        email: createGuserInput.email,
        name: createGuserInput.name,
      },
    });
  }
}

http://localhost:3000/users へ アクセスしてユーザ一覧が返ってくることを確認します。

● 7. NestJSの graphql エンドポイントの作成(code first)

graphql関連パッケージのインストール

npm i @nestjs/graphql @nestjs/apollo graphql

NestJS の起動

schema.gql を自動生成させるために、あらかじめサーバを起動しておきます

npm run start:dev

起動後に必ずエラーが出ていないことを確認しましょう。

graphql 関連ファイルの自動生成

npx nest generate resource gusers

(GraphQL (code first) を選択してエンターを押します)

以下のファイルが新規作成または更新されます

CREATE src/gusers/gusers.module.ts (238 bytes)
CREATE src/gusers/gusers.resolver.spec.ts (545 bytes)
CREATE src/gusers/gusers.resolver.ts (1181 bytes)
CREATE src/gusers/gusers.service.spec.ts (467 bytes)
CREATE src/gusers/gusers.service.ts (653 bytes)
CREATE src/gusers/dto/create-gguser.input.ts (198 bytes)
CREATE src/gusers/dto/update-gguser.input.ts (251 bytes)
CREATE src/gusers/entities/gguser.entity.ts (189 bytes)
UPDATE src/app.module.ts (454 bytes)

また schema.gql も生成されます。

1. Entity の修正

src/gusers/entities/guser.entity.ts を修正します

import { ObjectType, Field, Int } from '@nestjs/graphql';

@ObjectType()
export class Guser {
  @Field(() => Int, { description: 'Example field (placeholder)' })
  exampleField: number;
}

↓ 以下のように修正します

import { ObjectType, Field, ID } from '@nestjs/graphql';
import { Source } from '../../sources/entities/source.entity';

@ObjectType()
export class Guser {
  @Field(() => ID)
  id: number;

  @Field()
  name: string;

  @Field()
  email: string;

  @Field()
  createdAt: Date;

  @Field()
  updatedAt: Date;

  @Field(() => [Source], { nullable: true })
  source?: Array<Source>;
}

リレーション source も返すように設定します

npx nest generate resource sources

src/sources/entities/source.entity.ts

import { ObjectType, Field, ID } from '@nestjs/graphql';

@ObjectType()
export class Source {
  @Field(() => ID)
  id: number;

  @Field()
  useId: number;

  @Field()
  text: string;

  @Field()
  createdAt: Date;

  @Field()
  updatedAt: Date;

  @Field(() => [Source], { nullable: true })
  source?: Array<Source>;
}

2. Resolver / Service の修正

クエリの操作を行うリゾルバ / サービスを修正します。

リゾルバ:一旦そのままです。変更しません。

サービス: 以下のように修正します

src/gusers/gusers.service.ts

  findAll() {
    return `This action returns all gusers`;
  }

findOne(id: number) {
    return `This action returns a #${id} guser`;
  }

↓ このように findAll() , findOne()メソッドを入れ替えます

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
  async findAll() {
    return prisma.user.findMany({
      include: {
        source: true,
      },
    });
  }

  async findOne(id: number) {
    return prisma.user.findFirst({ where: { id: id } });
  }

graphqlクエリの確認

http://localhost:3000/graphql へアクセスして

{
  gusers{
    id,name,email,createdAt,updatedAt
  }
}

クエリを投げて、正しくデータが返ってくることを確認します。

● @Query の名前を変更する

getSourcesgetMySources に名前変更するには以下のようにします

  @Query(() => [Source])
  getSources(@CurrentUser() firebaseUser: CurrentFirebaseUserData) {
    ........
  }

方法1. { name: 'getMySources' } で変える

  @Query(() => [Source], { name: 'getMySources' })
  getSources(@CurrentUser() firebaseUser: CurrentFirebaseUserData) {
    ........
  }

方法2. メソッド名を変える

  @Query(() => [Source])
  getMySources(@CurrentUser() firebaseUser: CurrentFirebaseUserData) {
    ........
  }

その他参考: omar-dulaimi/prisma-class-validator-generator: Prisma 2+ generator to emit typescript models of your database with class validator
Prisma でメソッドはやせない問題どうしたらいいんだ | きむそん.dev

添付ファイル1
No.2305
06/02 09:43

edit

添付ファイル