GraphQL+NestJS+TypeORM+MySQL+Serverless(offline)の構成でデータベース接続を考える

## はじめに

この記事の続きです。

https://qiita.com/shinobu_shiva/items/d1df4682a124f45ef046

前の記事でGraphQL+NestJS+TypeORM+MySQLという構成に入門したのですが、これをServerless Frameworkで動かしたいと思います。

https://www.serverless.com/

今回の成果物はこちらにあります。

https://github.com/shinobushiva/nestjs-typeorm-ts-example/tree/step2

## Serverless Framework(offline)の導入とNestJSへのつなぎ込み

Serverless Frameworkをインストールします。
```
$ npm install -g serverless
```

前回作成したNestJSプロジェクトのルートフォルダで以下を実行してパッケージを追加します。

```
$ yarn add aws-lambda aws-serverless-express express
$ yarn add -D @types/aws-serverless-express serverless-layers
```

`serverless.yml` を作成します。

```yaml: serverless.yml
org: shinobushiva
app: myapp
service: compnestjs-typeorm-ts-example
frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs14.x
  region: ap-northeast-1
  stage: dev

functions:
  handler:
    handler: dist/handler.handler
    events:
      - http:
          cors: true
          path: "/"
          method: any
      - http:
          cors: true
          path: "{proxy+}"
          method: any
    environment:
      DB_HOST: ${env:DB_HOST}
      DB_PORT: ${env:DB_PORT}
      DB_USERNAME: ${env:DB_USERNAME}
      DB_PASSWORD: ${env:DB_PASSWORD}
      DB_DATABASE: ${env:DB_DATABASE}
plugins:
  - serverless-offline
```

`src/handler.ts` を作成します。

```ts:src/handler.ts
import { Context, Handler } from 'aws-lambda';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Server } from 'http';
import { ExpressAdapter } from '@nestjs/platform-express';
import * as serverless from 'aws-serverless-express';
import * as express from 'express';

let cachedServer: Server;

async function bootstrapServer(): Promise<Server> {
  const expressApp = express();
  const adapter = new ExpressAdapter(expressApp);
  const module = AppModule;
  const app = await NestFactory.create(module, adapter);
  await NestFactory.createApplicationContext(module);
  app.enableCors();
  await app.init();
  return serverless.createServer(expressApp);
}

export const handler: Handler = async (event: any, context: Context) => {
  if (!cachedServer) {
    cachedServer = await bootstrapServer();
  }

  return serverless.proxy(cachedServer, event, context, 'PROMISE').promise;
};

```

serverless-offline プラグインを導入

```
$ yarn add -D serverless-offline
```

ビルドして実行します。

```
$ yarn build & serverless offline
```

```console:database connection error
[Nest] 35703  - 2022/05/01 15:40:59   ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
AlreadyHasActiveConnectionError: Cannot create a new connection named "default", because connection with such name already exist and it now has an active connection session.
```

ここまでで `serverless` が起動して`http://localhost:3000/dev/graphql`にGraphQLプレイグラウンドが 立ち上がるのですが、コンソールには次のようなエラーが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/5b6137c2-0152-f3d2-395f-a58032ea6874.png)

## データベース接続エラーを解決する

このエラーはServerless Frameworkがデータベース接続を複数回行おうとしてTypeORMに同じ名前でコネクション貼ることは出来ないよ、と怒られているような状況です。

いくつか解決策はあると思うのですが、今回 `TypeOrmModule` の設定方法を修正することで解決します。

変更前の `src/app.module.ts` は次のようになっています。

```ts:src/app.module.ts 変更前
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import ormconfig from './config/ormconfig';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/tasks.module';

@Module({
  imports: [
    TypeOrmModule.forRoot(ormconfig),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      debug: true,
      playground: true,
    }),
    TasksModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

まず、 データベース接続を設定するクラスを `src/config/database.ts` に作成します。

```ts:src/config/database.ts
import { Injectable } from '@nestjs/common'
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'
import { ConnectionManager, getConnectionManager } from 'typeorm'
import ormconfig from './ormconfig'

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  async createTypeOrmOptions(): Promise<TypeOrmModuleOptions> {
    const ormOptions: TypeOrmModuleOptions = {
      ...ormconfig,
      keepConnectionAlive: true,
      autoLoadEntities: true,
    }

    const connectionManager: ConnectionManager = getConnectionManager()
    let options: any

    if (connectionManager.has('default')) {
      options = connectionManager.get('default').options
    } else {
      options = ormOptions as TypeOrmModuleOptions
    }
    return options
  }
}

```

このクラスはデータベース接続のためのオプションを返すもので、 `default` 接続が存在する場合は既存のオプションを返すようになっています。 `getConnectionManager` などが deprecatedになっているのですが、現状では新しい書き方がわからないのでこのままにしています。

これを使って `src/app.module.ts`を次のように修正します。

```ts:src/app.module.ts 変更後
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/tasks.module';
import { TypeOrmConfigService } from './config/database';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [TasksModule],
      useClass: TypeOrmConfigService,
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      debug: true,
      playground: true,
    }),
    TasksModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

再度ビルドして実行すると接続エラーは解消されています。

```
$ yarn build & serverless offline
```

## No metadata for "EntityName" was found

これで完成かと思って意気揚々とGraphQLのクエリを実行すると次のエラーに遭遇します。

```
"No metadata for \"Task\" was found."
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/4192716e-9574-4236-ba2a-fbd030943d63.png)

これが大変にわかりにくいエラーで解決に丸１日潰しました。

### No metadata エラーがなぜ発生するか調べる

エラー発生時のスタックトレースは次のようになっています。

```console:No metadata エラー
[Nest] 51685  - 2022/05/01 16:20:33   ERROR [ExceptionsHandler] No metadata for "Task" was found.
EntityMetadataNotFoundError: No metadata for "Task" was found.
    at DataSource.getMetadata (/Users/izumi/Programs/nestjs-typeorm-ts-example/node_modules/typeorm/data-source/DataSource.js:286:19)
    at Repository.get metadata [as metadata] (/Users/izumi/Programs/nestjs-typeorm-ts-example/node_modules/typeorm/repository/Repository.js:23:40)
    at Repository.find (/Users/izumi/Programs/nestjs-typeorm-ts-example/node_modules/typeorm/repository/Repository.js:167:39)
```

該当部分のコード(JS）に飛んでみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/56f8de63-979c-72e8-c3f8-8d4ba55d6d8a.png)

`metadata`が取得できていないようなので、`findMetadata`を見に行きます。

```js:findMetadata
      findMetadata(target) {
        return this.entityMetadatas.find((metadata) => {
            if (metadata.target === target)
                return true;
            if (InstanceChecker_1.InstanceChecker.isEntitySchema(target)) {
                return metadata.name === target.options.name;
            }
            if (typeof target === "string") {
                if (target.indexOf(".") !== -1) {
                    return metadata.tablePath === target;
                }
                else {
                    return (metadata.name === target ||
                        metadata.tableName === target);
                }
            }
            if (ObjectUtils_1.ObjectUtils.isObject(target) &&
                typeof target.name === "string") {
                if (target.name.indexOf(".") !== -1) {
                    return metadata.tablePath === target.name;
                }
                else {
                    return (metadata.name === target.name ||
                        metadata.tableName === target.name);
                }
            }
            return false;
        });
    }
```

どうやら `entityMetadatas` から該当の `metadata` を探し出すメソッドのようです。

```js:findMetadata デバッグ出力埋め込み
    findMetadata(target) {
        return this.entityMetadatas.find((metadata) => {
            console.log('findMetadata', metadata.target, target, metadata.target === target)
            if (metadata.target === target)
                return true;
            if (InstanceChecker_1.InstanceChecker.isEntitySchema(target)) {
                return metadata.name === target.options.name;
            }
```

このようにデバッグ出力を埋め込んで実行してみると次のような結果になりました。

```console: デバッグ出力
findMetadata [class Task] [class Task] false
```

一見、 `metadata.target`, `target` は同じclassのようですが、比較結果が `false` となっています。おそらくクラスの読み込みの関係（内部的にはFunction?）で別の参照先になっているのが原因で違うものと判定された結果`metadata`が見つからないという実行結果になるようです。


### 解決策１（たぶんあまり良くない）

```js:findMetadata デバッグ出力埋め込み
    findMetadata(target) {
        return this.entityMetadatas.find((metadata) => {
            console.log('findMetadata', metadata.target, target, metadata.target === target)
            if (metadata.target === target)
                return true;
            if (metadata.target.toString() === target.toString())
                return true;
```

一つの解決策として参照で比較するのではなくて文字列として比較することを考えました。

これで解決します。解決しますが、Javascript力の低い私には正直これでいいのかよくわかりません。また、これだとTypeORMにこの修正を取り込んでもらわないといけません。

ということで、これは諦めて別の方法を考えます。

### Repository側からの解決を考える

もう一度 No metadata エラーのスタックトレースをよく見ると `Repository.get metadata` となっています。

```console:No metadata エラー
[Nest] 51685  - 2022/05/01 16:20:33   ERROR [ExceptionsHandler] No metadata for "Task" was found.
EntityMetadataNotFoundError: No metadata for "Task" was found.
    at DataSource.getMetadata (/Users/izumi/Programs/nestjs-typeorm-ts-example/node_modules/typeorm/data-source/DataSource.js:286:19)
    at Repository.get metadata [as metadata] (/Users/izumi/Programs/nestjs-typeorm-ts-example/node_modules/typeorm/repository/Repository.js:23:40)
    at Repository.find (/Users/izumi/Programs/nestjs-typeorm-ts-example/node_modules/typeorm/repository/Repository.js:167:39)
```

どうやら `metadata` は `Repository` から取得するようです。

`Repository` について詳しくはこのあたりを参考にしてください。

https://docs.nestjs.com/recipes/sql-typeorm#repository-pattern

さて、`Task`の`Repository`はどこで設定されているかと探してみると、`src/tasks/tasks.service.ts`にこのような記述がありました。

```ts:src/tasks/tasks.service.ts 一部
export class TasksService {
  constructor(
    @InjectRepository(Task)
    private taskRepostiory: Repository<Task>,
  ) {}
```

```ts:src/tasks/tasks.service.ts 全体（修正前）
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTaskInput } from './dto/create-task.input';
import { UpdateTaskInput } from './dto/update-task.input';
import { Task } from './entities/task.entity';

@Injectable()
export class TasksService {
  constructor(
    @InjectRepository(Task)
    private taskRepostiory: Repository<Task>,
  ) {}

  async create(createTaskInput: CreateTaskInput) {
    const task = this.taskRepostiory.create(createTaskInput);
    await this.taskRepostiory.save(task);
    return task;
  }

  findAll() {
    return this.taskRepostiory.find();
  }

  async findOne(id: number) {
    return await this.taskRepostiory.findOne({
      where: {
        id,
      },
    });
  }

  async update(id: number, updateTaskInput: UpdateTaskInput) {
    const task = this.findOne(id);
    if (task) {
      await this.taskRepostiory.save(updateTaskInput);
    }
  }

  async remove(id: number) {
    const result = await this.taskRepostiory.delete(id);
    return result.affected > 0;
  }
}

```
いかにも`@InjectRepository(Task)`のデコーレータ部分が`repository`を設定していそうです。`InjectRepository`の中身を読んでみたのですが、正直良くわかりませんでした。

### 解決策２（今の所マシな感じ）

とはいえ`taskRepostiory`の中身をどうにかすればうまくいくかもしれないということで、色々試した結果この様になりました。

```ts:src/tasks/tasks.service.ts 全体（修正後）
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { create } from 'domain';
import { async } from 'rxjs';
import { ConnectionManager, getConnectionManager, getRepository, Repository } from 'typeorm';
import { CreateTaskInput } from './dto/create-task.input';
import { UpdateTaskInput } from './dto/update-task.input';
import { Task } from './entities/task.entity';

@Injectable()
export class TasksService {
  // constructor(
  //   @InjectRepository(Task)
  //   private taskRepostiory: Repository<Task>,
  // ) {}
  get taskRepostiory(): Repository<Task> {
    const connectionManager: ConnectionManager = getConnectionManager();
    if (connectionManager.has('default')) {
      const entityMetadata = connectionManager
        .get('default')
        .entityMetadatas.find((metadata) => {
          return metadata.tableName === 'task';
        });
      if (entityMetadata) {
        return getRepository(entityMetadata.name);
      } else {
        return null;
      }
    }
  }

  async create(createTaskInput: CreateTaskInput) {
    const task = this.taskRepostiory.create(createTaskInput);
    await this.taskRepostiory.save(task);
    return task;
  }

  findAll() {
    return this.taskRepostiory.find();
  }

  async findOne(id: number) {
    return await this.taskRepostiory.findOne({
      where: {
        id,
      },
    });
  }

  async update(id: number, updateTaskInput: UpdateTaskInput) {
    const task = this.findOne(id);
    if (task) {
      await this.taskRepostiory.save(updateTaskInput);
    }
  }

  async remove(id: number) {
    const result = await this.taskRepostiory.delete(id);
    return result.affected > 0;
  }
}

```

コンストラクタでデコレーターを使って設定されていた`taskRepostiory`をgetterに置き換えました。

これで No metadata エラーが発生しなくなりました。 `serverless offline`での実行、`nest start --watch`での実行どちらも動作するようになりました。

## まとめ
ということで、なんとか動作するところまでこぎつけられました。もっと良い解決策があるんじゃないかと思いつつ、現時点での私としては限界点です。スマートな解決策お持ちの方、ぜひご教示ください。