NestJSにMikroORMの導入手順。CRUDのAPIを実装するまで。

NestJSにMikroORMの導入手順。CRUDのAPIを実装するまで。

MikroORMNestJSTypeScript

おはこんにちばんは。最近、NextJSでMikroORMを利用する機会があったので、導入手順をご紹介しようと思います。

今回はNestJS + MikroORM + PostgreSQL を利用していきます。サンプルのコードはこちら

環境

  • Node.js v12.18.0
  • PostgreSQL 10.18
  • macOS Big Sur

インストール

以下のコマンドでプロジェクトを作成します。今回はyarnにしています。

nest new nestjs-example-app


一応、動作することだけ確認しとくと良いでしょう。

cd nestjs-example-app
yarn start:dev


コンソールでNest application successfully startedが表示後、以下を実行してHello World!が表示されたらOKです!

curl -X GET http://localhost:3000

事前準備

データベースを作成

以下のようにデータベースを作成します。データベースの作り方は普段やっている方法で構いません。

createdb example

パッケージをインストール

必要なパッケージをインストールします。

yarn add mikro-orm @mikro-orm/core @mikro-orm/nestjs @mikro-orm/postgresql @mikro-orm/migrations @mikro-orm/reflection @mikro-orm/sql-highlighter @nestjs/config

仮のAPI作成

では、まず簡単なテキストを返す仮のAPIを作っていきましょう!

モジュールを作成

以下のコマンドでモジュールを作成します。ファイルが作成されたら成功です!

nest g module example

CREATE src/example/example.module.ts (84 bytes)
UPDATE src/app.module.ts (320 bytes)

コントローラを作成

以下のコマンドでコントローラを作成します。ファイルが作成されたら成功です!

nest g controller example --no-spec

CREATE src/example/example.controller.ts (103 bytes)
UPDATE src/example/example.module.ts (178 bytes)

コントローラを更新

まずは、簡単なテキストだけを返すAPIを用意します。ルーティングは以下になります。

  • /example(GET):登録されたexampleを全て返す
  • /example/:id(GET):[id]のexampleを返す
  • /example(POST):exampleの新規登録
  • /example/:id(PATCH):[id]のexampleを更新
  • /example/:id(DELETE):[id]のexampleを削除
src/example/example.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { CreateExampleRequestDto } from './dto/create-example-request.dto';
import { UpdateExampleRequestDto } from './dto/update-example-request.dto';


@Controller('example')
export class ExampleController {
  @Post()
  async create(@Body() dto: CreateExampleRequestDto) {
    return `create example ${dto.name}`;
  }


  @Get(':id')
  async findOne(@Param('id') id: string) {
    return `get example ${id}`;
  }


  @Get('')
  async findAll() {
    return `get example all`;
  }


  @Patch(':id')
  update(@Param('id') id: string, @Body() dto: UpdateExampleRequestDto) {
    return `update example ${dto.name}`;
  }


  @Delete(':id')
  remove(@Param('id') id: string) {
    return `delete example ${id}`;
  }
}
src/example/dto/create-example-request.dto

export type CreateExampleRequest = {
  name: string;
};

export class CreateExampleRequestDto implements CreateExampleRequest {
  public readonly name: string;
}
src/example/dto/update-example-request.dto

export type UpdateExampleRequest = {
  id: string;
  name: string;
};

export class UpdateExampleRequestDto implements UpdateExampleRequest {
  public readonly id: string;
  public readonly name: string;
}


動作確認

実際にテキストが返ってくるか確認してみましょう。以下のcurlを叩いて期待する値が返ってくればOKです!
これで仮のAPIは一旦終了です。

$ curl -X GET http://localhost:8080/example
get example all

$ curl -X GET http://localhost:8080/example/1
get example 1

$ curl -X POST http://localhost:8080/example -d 'name=TEST'
create example TEST

$ curl -X PATCH http://localhost:8080/example/1 -d 'name=TEST'
update example TEST

$ curl -X DELETE http://localhost:8080/example/1
delete example 1

MikroORMの設定

次にMikroORMの設定周りの手順を説明してきます。

MikroORMの準備

MikroORMの設定ファイルを準備します。

src/mikro-orm.config.ts

import { Logger } from '@nestjs/common';
import { Options } from '@mikro-orm/core';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { TsMorphMetadataProvider } from '@mikro-orm/reflection';
import { SqlHighlighter } from '@mikro-orm/sql-highlighter';

const logger = new Logger('MikroORM');

const config: Options<PostgreSqlDriver> = {
  driver: PostgreSqlDriver,
  metadataProvider: TsMorphMetadataProvider,
  highlighter: new SqlHighlighter(),
  debug: true,
  logger: logger.log.bind(logger),
  entities: ['dist/**/*.entity.js'],
  entitiesTs: ['src/**/*.entity.ts'],
  baseDir: process.cwd(),
};

export default config;


そして、MikroORMのモジュールを利用できるようにapp.module.tsにimportします!

src/app.module.ts

import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ExampleModule } from './example/example.module';
import config from './mikro-orm.config';


@Module({
  imports: [MikroOrmModule.forRoot(config), ExampleModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

環境変数の設定

DB周りの指定は環境変数で渡せるようにした方が楽なので、渡せるようにします!app.module.tsConfigModuleをimportします。

src/app.module.ts

import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ExampleModule } from './example/example.module';
import config from './mikro-orm.config';

@Module({
  imports: [
    MikroOrmModule.forRoot(config),
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    ExampleModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}


環境変数は以下のようにDBの設定情報を追加します。

./.env

MIKRO_ORM_DB_NAME=example
MIKRO_ORM_PORT=5432

package.jsonの修正

MikroORM CLIはTypeScriptで書いたmikro-orm.config.tsが認識できないので、package.jsonに下記を追加します。

{
  /*...*/
  "mikro-orm": {
    "useTsNode": true,
    "configPaths": [
      "./src/mikro-orm.config.ts",
      "./dist/mikro-orm.config.js"
    ]
  }
}

Entityの作成

entityがないと動かないので以下を追加していきます。

./src/example/example.entity.ts

import { BigIntType, Entity, PrimaryKey, Property } from '@mikro-orm/core';

@Entity()
export class Example {
  @PrimaryKey({ type: BigIntType })
  id: string;

  @Property()
  name!: string;
}


この状態でyarn start:devしてみましょう!特にエラーが出なければOKです。

マイグレーションの設定

MikroORMの設定が出来ましたら、アプリケーションを実行時にマイグレーションが流れるようにしたいので、その設定を行っていきます。

Configファイルの更新

マイグレーションに関する設定をmikro-orm.config.tsに追加していきます。細かい設定内容は公式ドキュメントを見るといいでしょう。

src/mikro-orm.config.ts

import { Logger } from '@nestjs/common';
import { Options } from '@mikro-orm/core';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { TsMorphMetadataProvider } from '@mikro-orm/reflection';
import { SqlHighlighter } from '@mikro-orm/sql-highlighter';

const logger = new Logger('MikroORM');

const config: Options<PostgreSqlDriver> = {
  driver: PostgreSqlDriver,
  metadataProvider: TsMorphMetadataProvider,
  highlighter: new SqlHighlighter(),
  debug: true,
  logger: logger.log.bind(logger),
  entities: ['dist/**/*.entity.js'],
  entitiesTs: ['src/**/*.entity.ts'],
  baseDir: process.cwd(),
  migrations: {
    tableName: 'schema_version',
    path: './src/migrations',
    pattern: /^[\w-]+\d+\.ts$/,
    transactional: true,
    disableForeignKeys: true,
    allOrNothing: true,
    dropTables: true,
    safe: false,
    emit: 'ts',
  },
  forceUtcTimezone: true,
};

export default config;

マイグレーションファイルの作成

MikroORM CLIで用意されているので、以下を実行しましょう!

npx mikro-orm migration:create


これで以下のようなファイルが作成れると成功です。

src/migrations/Migration20210925112002.ts

マイグレーションの実行

最後にアプリケーション実行時にマイグレーションが実行されるようにします。
MikroORM CLIで実行は出来るのですが、公式が推奨していないので以下のようなスクリプトを用意します。

import { MikroORM } from '@mikro-orm/core';
import config from './mikro-orm.config';

(async () => {
  const orm = await MikroORM.init(config);

  const migrator = orm.getMigrator();
  await migrator.up();
  await orm.close(true);
})();


最後にpackage.jsonを変更します。

{
  /*...*/
  "scripts": {
    "start": "yarn migration:dev && nest start",
    "start:dev": "yarn migration:dev && nest start --watch",
    "start:debug": "yarn migration:dev && nest start --debug --watch",
    "start:prod": "yarn migration:prod && node dist/main",
    "migration:create": "npx mikro-orm migration:create",
    "migration:dev": "ts-node src/migration",
    "migration:test": "ts-node src/migration",
    "migration:prod": "node dist/migration",
  }
}


これでyarn start:dev するとマイグレーションが実行されれば成功です!

一応DBでも以下のように追加されていればOKです。

$ psql example

example=# \d
                    List of relations
 Schema |         Name          |   Type   |    Owner    
--------+-----------------------+----------+-------------
 public | example               | table    | keppledev04
 public | example_id_seq        | sequence | keppledev04
 public | schema_version        | table    | keppledev04
 public | schema_version_id_seq | sequence | keppledev04
(4 rows)

example=# \d example
                                    Table "public.example"
 Column |          Type          | Collation | Nullable |               Default               
--------+------------------------+-----------+----------+-------------------------------------
 id     | bigint                 |           | not null | nextval('example_id_seq'::regclass)
 name   | character varying(255) |           | not null | 
Indexes:
    "example_pkey" PRIMARY KEY, btree (id)

サービスを追加

それでは、DBに接続するためにサービス周りを整備していきます。

サービスの作成

以下のコマンドで作成します!

nest g service example --no-spec

サービスの修正

以下のようにCRUDが行える関数を追加します!

src/example/example.service.ts

import { InjectRepository } from '@mikro-orm/nestjs';
import { Injectable, NotFoundException } from '@nestjs/common';
import { EntityRepository, wrap } from 'mikro-orm';
import { CreateExampleRequestDto } from './dto/create-example-request.dto';
import { CreateExampleResponseDto } from './dto/create-example-response.dto';
import { GetExampleResponseDto } from './dto/get-example-response.dto';
import { UpdateExampleRequestDto } from './dto/update-example-request.dto';
import { UpdateExampleResponseDto } from './dto/update-example-response.dto';
import { Example } from './example.entity';

@Injectable()
export class ExampleService {
  constructor(
    @InjectRepository(Example)
    private readonly exampleRepository: EntityRepository<Example>,
  ) {}

  public async create(dto: CreateExampleRequestDto) {
    const { name } = dto;
    const example = new Example();
    example.name = name;
    await this.exampleRepository.persistAndFlush(example);
    return new CreateExampleResponseDto(example);
  }

  public async findOne(id: string) {
    const example = await this.exampleRepository.findOne(id);
    if (!example) throw new NotFoundException();

    return new GetExampleResponseDto(example);
  }

  public async findAll() {
    const examples = await this.exampleRepository.findAll();
    return examples.map((example) => new GetExampleResponseDto(example));
  }

  public async update(id: string, dto: UpdateExampleRequestDto) {
    const example = await this.exampleRepository.findOne(id);
    const nextExample = { ...example, name: dto.name };
    wrap(example).assign(nextExample);
    await this.exampleRepository.flush();

    return new UpdateExampleResponseDto(nextExample);
  }

  public async remove(id: string) {
    const example = await this.exampleRepository.findOne(id);
    await this.exampleRepository.removeAndFlush(example);
  }
}
src/example/dto/create-example-response.dto.ts

export type CreateExampleResponse = {
  id: string;
  name: string;
};

export class CreateExampleResponseDto implements CreateExampleResponse {
  public readonly id: string;
  public readonly name: string;

  public constructor(object: CreateExampleResponse) {
    Object.assign(this, object);
  }
}
src/example/dto/update-example-response.dto.ts

export type UpdateExampleResponse = {
  id: string;
  name: string;
};

export class UpdateExampleResponseDto implements UpdateExampleResponse {
  public readonly id: string;
  public readonly name: string;

  public constructor(object: UpdateExampleResponse) {
    Object.assign(this, object);
  }
}
src/example/dto/get-example-response.dto.ts

import { Example } from '../example.entity';

export type GetExampleResponse = {
  id: string;
  name: string;
};

export class GetExampleResponseDto implements GetExampleResponse {
  public readonly id: string;
  public readonly name: string;

  public constructor(object: Example) {
    Object.assign(this, object);
  }
}


最後にExampleServiceでMikroORMを利用できるようにexample.module.tsのimportsを以下に変更します。これでサービス回りは終了です。

./src/example/example.module.ts

import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { ExampleController } from './example.controller';
import { Example } from './example.entity';
import { ExampleService } from './example.service';

@Module({
  imports: [MikroOrmModule.forFeature({ entities: [Example] })],
  controllers: [ExampleController],
  providers: [ExampleService],
})
export class ExampleModule {}

仮のAPIをCRUDのAPIに修正

最後に各ルーティングからCRUDの処理を実行できるようにします。example.controller.ts を以下のように修正します。

./src/api/example/example.controller.ts

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { CreateExampleRequestDto } from './dto/create-example-request.dto';
import { UpdateExampleRequestDto } from './dto/update-example-request.dto';
import { ExampleService } from './example.service';

@Controller('example')
export class ExampleController {
  constructor(private readonly exampleService: ExampleService) {}

  @Post()
  async create(@Body() dto: CreateExampleRequestDto) {
    return await this.exampleService.create(dto);
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    return await this.exampleService.findOne(id);
  }

  @Get('')
  async findAll() {
    return await this.exampleService.findAll();
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() dto: UpdateExampleRequestDto) {
    return this.exampleService.update(id, dto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.exampleService.remove(id);
  }
}


これで完了です。 yarn start:dev を実行しcurlで確認してみましょう。以下のようになれば成功です。お疲れ様でした。

$ curl -X POST http://localhost:8080/example -d 'name=TEST'
{"name":"TEST","id":"1"}  

$ curl -X POST http://localhost:8080/example -d 'name=TEST'
{"name":"TEST","id":"2"}  

$ curl -X GET http://localhost:8080/example
[{"id":"1","name":"TEST"},{"id":"2","name":"TEST"}]

$ curl -X GET http://localhost:8080/example/1
{"id":"1","name":"TEST"}

$ curl -X PATCH http://localhost:8080/example/1 -d 'name=TEST2'
{"id":"1","name":"TEST2"}

$ curl -X DELETE http://localhost:8080/example/1
$ curl -X GET http://localhost:8080/example
[{"id":"2","name":"TEST"}]

さいごに

TypeScriptのORMであるMikroORMの導入手順についてご紹介しました。PrismaTypeORMに比べてシンプルかつトランザクションなど色々と扱いやすかったです。日本では使われているケースをあまり見ないですが、選択の一つとして是非試してみください!