NestJSをAPI Gateway + AWS Lambdaで構成する（その７）ORM（RDBデータベースを扱う）

#
- [その１ NestJSやLambdaでのnode.jsの話し](https://qiita.com/ssugimoto/items/988320d34022555401fb)
- [その２ NestJSのサンプルを動かす、Hello World.](https://qiita.com/ssugimoto/items/557e7a7cce5111bf5de7)
- [その３ コントローラーを追加する](https://qiita.com/ssugimoto/items/fcfbc9a176bcf41d7b45)
- [その４ DTOを使う](https://qiita.com/ssugimoto/items/613b8ab54bd6f8de043c)
- [その５ CRUD generatorを使い modules,controller, service, dto を生成する](https://qiita.com/ssugimoto/items/64a9c3cd5cb33e06f4c9)
- [その６ インターセプタ](https://qiita.com/ssugimoto/items/dce542a78fdfbaf1e434)
- その７ ORMでRDBデータベースを扱う
- [その８ NestJSをAPI Gateway＋AWS Lambdaとしてデプロイ](https://qiita.com/ssugimoto/items/74c26a8219a944d4acc4)

## ドキュメント
- 公式 https://docs.nestjs.com/techniques/database
    - [TypeORM](https://docs.nestjs.com/recipes/sql-typeorm)
    - [Prisma](https://docs.nestjs.com/recipes/prisma)
- 公式は、mysql 使ってます
- 今回は、公式とは異なるPostgreSQL使ってみます

## TypeORMとPrisma
- https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm
- Prismaの方が新しい

## 1. Prismaセットアップ、既存 nestjsのプロジェクトに追加する
```
$ npm install -g @nestjs/cli
$ nest new hello-prisma
```
ここまでは新規の場合
その６までで使ってきた「aws-node-typescript-nest」を引き続き利用します

```
$ cd aws-node-typescript-nest
$ npm install prisma --save-dev
```

以下のバージョンがインストールされた
```
"prisma": "^5.0.0",
```

### セットアップつづき
```
$ npx prisma init
```

- 以下、initの実行ログ
```
$  npx prisma init

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started
    
```

以下のファイルが作成される

```
aws-node-typescript-nest/.env
aws-node-typescript-nest/prisma/schema.prisma
```
- postgresqlは選んでないけど、デフォルトは`postgresql`でファイルが生成される


## 2. postgresqlのセットアップ（postgresql dockerコンテナ利用）

- postgresqlはコンテナを使い、docker composeを使う
- postgresql接続用のIDE・ツールはpgadmin4を使う
- postgresqlとpgadminは以下の定義相当
```compose.yml
services:
  postgresql:
    image: postgres:15.3
    container_name: postgresql
    ports:
      - 5432:5432
    volumes:
      #- ./contrib/setup.sh:/docker-entrypoint-initdb.d/initdb.sh
      - ./postgres/pgdata:/var/lib/postgresql/data
      - ./postgres/init:/docker-entrypoint-initdb.d
      - ./postgres/config/postgresql.conf:/etc/postgresql/postgresql.conf
    environment:
      POSTGRES_USER: pguser
      POSTGRES_PASSWORD: pgpassword
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
      POSTGRES_DB: mydb
      POSTGRES_HOST_AUTH_METHOD: trust
    command: postgres -c log_destination=stderr -c log_statement=all -c log_connections=on -c log_disconnections=on -c 'config_file=/etc/postgresql/postgresql.conf'
    hostname: postgresql
    restart: always

  pgadmin4:
    image: dpage/pgadmin4:latest
    container_name: pgadmin4
    ports:
      - 8000:80
    volumes:
      - ./pgadmin:/var/lib/pgadmin/storage
    environment:
      PGADMIN_DEFAULT_EMAIL: root@myapp.com
      PGADMIN_DEFAULT_PASSWORD: root
    hostname: pgadmin4
    depends_on:
      - postgresql
    restart: always
```

- コンテナの起動（docker compose利用）
```
# compose.yml ファイルのある場所に移動
cd xxx 
docker compose up -d
```
- pgadmin4へアクセス
    - http://localhost:8000/
    - compose.ymlのpgadmin4のenvironmentで指定する
    - ログイン`root@myapp.com` と パスワード`root` で ログインする
- compose.ymlのenvironmentで指定している情報で接続します
    - hostname ：postgresql
    - port : 5432
    - Maintenace databasename：mydb `POSTGRES_DBとして指定した値`
    - Username：POSTGRES_USERとして指定した値
    - Password：POSTGRES_PASSWORDとして指定した値
    <img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482752/a0dc4066-2b44-f4e5-fd44-ff1f20fa79b9.png" width=80% />
    <img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482752/b8e8be3a-f06d-8a12-23f0-fa9202d4a8c1.png" width=80% /> 


- コンテナ停止する場合は以下コマンドで可能
```
docker compose down
```

## 3. nestjs側の設定 postgresql接続設定

```aws-node-typescript-nest/.env

DATABASE_URL="postgresql://pguser:pgpassword@localhost:5432/mydb?schema=public"
```

- 記載する各項目の値
```
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public"
```


## 4. Prismaのschema.prismaに２つのテーブル定義を記載して、migrateをしてみる

```aws-node-typescript-nest/prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

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

model User {
  id    Int     @default(autoincrement()) @id
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int      @default(autoincrement()) @id
  title     String
  content   String?
  published Boolean? @default(false)
  author    User?    @relation(fields: [authorId], references: [id])
  authorId  Int?
}
```
### migrate実行
```
 npx prisma migrate dev --name init
```

- postgresqlへの通信ができないと以下のようなエラーになる
```
$ npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "mydb", schema "public" at "localhost:5432"

Error: P1001: Can't reach database server at `localhost`:`5432`

Please make sure your database server is running at `localhost`:`5432`.
```

- アプリ側を別のdev-containerを使っていて、docker composeで管理されていないので アプリのコンテナ -> ホスト -> docker compose管理のコンテナ への接続ができない。回避は https://docs.docker.com/desktop/networking/ を例に
```
DATABASE_URL="postgresql://pguser:pgpassword@host.docker.internal:5432/mydb?schema=public"
```

- 実行ログ
```
root ➜ /workspaces/serverless-nest/sls-examples/aws-node-typescript-nest (feature/v3-aws-nest-prisma ✗) $ npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "mydb", schema "public" at "host.docker.internal:5432"

Applying migration `20230727044313_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20230727044313_init/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)

added 2 packages, and audited 1817 packages in 5s

153 packages are looking for funding
  run `npm fund` for details

47 vulnerabilities (11 low, 17 moderate, 19 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

✔ Generated Prisma Client (5.0.0 | library) to ./node_modules/@prisma/client in 53ms
```

- postgres側のpublicスキーマを参照するとテーブルが作成されている
    - <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482752/0523f135-0430-4fb3-3e30-6283f41e2283.png" width="30%">
    - <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1482752/8a303db3-497d-843d-ca30-6b0deffc9b6f.png" width="30%">

## 5. Prsimaのデータベース操作用のモジュールを追加する、Install and generate Prisma Client
- Prisma Clientは、Prismaモデル定義から生成されるタイプセーフのデータベースクライアント
- clinetモジュールを追加
```
npm install @prisma/client
``` 

- DB接続用クライアントを生成する
```
npx prisma generate 
```

- 実行ログ例
```
$ npx prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma

✔ Generated Prisma Client (5.0.0 | library) to ./node_modules/@prisma/client in 53ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
\```
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
\```
```

- `./node_modules/@prisma/client` を見ると `aws-node-typescript-nest/node_modules/.prisma/client/index.d.ts` 一部抜粋

```aws-node-typescript-nest/node_modules/.prisma/client/index.d.ts

/**
 * Client
**/

import * as runtime from '@prisma/client/runtime/library';
import $Types = runtime.Types // general types
import $Public = runtime.Types.Public
import $Utils = runtime.Types.Utils
import $Extensions = runtime.Types.Extensions

export type PrismaPromise<T> = $Public.PrismaPromise<T>


export type UserPayload<ExtArgs extends $Extensions.Args = $Extensions.DefaultArgs> = {
  name: "User"
  objects: {
    posts: PostPayload<ExtArgs>[]
  }
  scalars: $Extensions.GetResult<{
    id: number
    email: string
    name: string | null
  }, ExtArgs["result"]["user"]>
  composites: {}
}

/**
 * Model User
 * 
 */
export type User = runtime.Types.DefaultSelection<UserPayload>
export type PostPayload<ExtArgs extends $Extensions.Args = $Extensions.DefaultArgs> = {
  name: "Post"
  objects: {
    author: UserPayload<ExtArgs> | null
  }
  scalars: $Extensions.GetResult<{
    id: number
    title: string
    content: string | null
    published: boolean | null
    authorId: number | null
  }, ExtArgs["result"]["post"]>
  composites: {}
}

/**
 * Model Post
 * 
 */
export type Post = runtime.Types.DefaultSelection<PostPayload>

...

```

## 6. PrismaServiceを作成する
- `npx prisma generate` または、 https://docs.nestjs.com/recipes/prisma の `use-prisma-client-in-your-nestjs-services` のソースを作成する


```aws-node-typescript-nest/src/prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}
```

## 7. PrismaServiceをモジュール等で使えるようにする、そしてDIする

- `aws-node-typescript-nest/src/users/users.service.ts`にconstructor追加、findOne() は asyncにしていなれば asyncにする。DBアクセスをawaitにできないとDBアクセス待たずに処理が次にいってしまう。

```users.service.ts
import { PrismaService } from '../prisma.service';
...

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

略
  async findOne(id: number) {
    const user = await this.prisma.user.findUnique({
      where: {
        id,
      },
    });
    console.log({ user })
    return `This action returns a #${id} user`;
  }
```

- `aws-node-typescript-nest/src/users/users.module.ts`にprovidersでPrismaServiceを追加
```users.module.ts
import { PrismaService } from '../prisma.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService, PrismaService]
})
export class UsersModule { }
```

- app.module.ts は UsersModuleをimportsするため変更なし
```aws-node-typescript-nest/src/app.module.ts
@Module({
  imports: [UsersModule],
  controllers: [AppController, CatsController],
  providers: [AppService, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, }],
})
export class AppModule { }

```

## 8. PostgreSQLデータベースにデータを入れておく

- 簡単なINSERT文の例
```insert.sql
INSERT INTO public."User"(
	id, email, name)
	VALUES (101, 'root-101@example.com', '101-name');

INSERT INTO public."User"(
	id, email, name)
	VALUES (102, 'root-102@example.com', '102-name');
```


## 9. NestJSのアプリケーションにリクエストを送ってみる

- npm startで起動しておき
- curlやpostmanで /users/{:id} にGETリクエストを送る
- URL 例、http://localhost:3000/dev/users/101, http://localhost:3000/dev/users/102

- コンソールにデータベースから取得した情報が表示される
```
{ user: { id: 101, email: 'root-101@example.com', name: '101-name' } }
{ user: { id: 102, email: 'root-102@example.com', name: '102-name' } }

```

- power shell からの curl実行した例
```
> curl  http://localhost:3000/dev/users/101

                                                                                                                        StatusCode        : 200                                                                                                 StatusDescription : OK                                                                                                  Content           : This action returns a #101 user
RawContent        : HTTP/1.1 200 OK
                    Connection: keep-alive
                    Keep-Alive: timeout=5
                    Accept-Ranges: bytes
                    Content-Length: 31
                    Cache-Control: no-cache
                    Content-Type: text/html; charset=utf-8
                    Date: Sat, 29 Jul 2023 05:33:...
Forms             : {}
Headers           : {[Connection, keep-alive], [Keep-Alive, timeout=5], [Accept-Ranges, bytes], [Content-Length, 31]...
                    }
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 31



> curl  http://localhost:3000/dev/users/102


StatusCode        : 200
StatusDescription : OK
Content           : This action returns a #102 user
RawContent        : HTTP/1.1 200 OK
                    Connection: keep-alive
                    Keep-Alive: timeout=5
                    Accept-Ranges: bytes
                    Content-Length: 31
                    Cache-Control: no-cache
                    Content-Type: text/html; charset=utf-8
                    Date: Sat, 29 Jul 2023 05:33:...
Forms             : {}
Headers           : {[Connection, keep-alive], [Keep-Alive, timeout=5], [Accept-Ranges, bytes], [Content-Length, 31]...
                    }
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 31

```


## 感想
- schema定義してマイグレーションを実行すると、Rails や Django に似てると思う。LL言語・スクリプト言語はおおよそ似てくるのか？
- 久しぶりにORMを使った
    - 日頃は、ORMに関連しなかったり、DynamoDBだったりしていたのでORMをゼロから触ったのは2016年頃のDBFlute、2019年頃のDjango 依頼かもしれない
- Amplify での schema.graphql もAppSyncのGraphQLでいくつかgenerateされる
- NestJSでPrismaの場合にフロントエンド用のts自動生成はあるのか？