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にしています。

1nest new nestjs-example-app

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

1cd nestjs-example-app
2yarn start:dev

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

1curl -X GET http://localhost:3000

事前準備

データベースを作成

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

1createdb example

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

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

1yarn 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を作っていきましょう!

モジュールを作成

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

1nest g module example
2
3CREATE src/example/example.module.ts (84 bytes)
4UPDATE src/app.module.ts (320 bytes)

コントローラを作成

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

1nest g controller example --no-spec
2
3CREATE src/example/example.controller.ts (103 bytes)
4UPDATE 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
1import {
2 Body,
3 Controller,
4 Delete,
5 Get,
6 Param,
7 Patch,
8 Post,
9} from '@nestjs/common';
10import { CreateExampleRequestDto } from './dto/create-example-request.dto';
11import { UpdateExampleRequestDto } from './dto/update-example-request.dto';
12
13
14@Controller('example')
15export class ExampleController {
16 @Post()
17 async create(@Body() dto: CreateExampleRequestDto) {
18 return `create example ${dto.name}`;
19 }
20
21
22 @Get(':id')
23 async findOne(@Param('id') id: string) {
24 return `get example ${id}`;
25 }
26
27
28 @Get('')
29 async findAll() {
30 return `get example all`;
31 }
32
33
34 @Patch(':id')
35 update(@Param('id') id: string, @Body() dto: UpdateExampleRequestDto) {
36 return `update example ${dto.name}`;
37 }
38
39
40 @Delete(':id')
41 remove(@Param('id') id: string) {
42 return `delete example ${id}`;
43 }
44}

src/example/dto/create-example-request.dto
1export type CreateExampleRequest = {
2 name: string;
3};
4
5export class CreateExampleRequestDto implements CreateExampleRequest {
6 public readonly name: string;
7}

src/example/dto/update-example-request.dto
1export type UpdateExampleRequest = {
2 id: string;
3 name: string;
4};
5
6export class UpdateExampleRequestDto implements UpdateExampleRequest {
7 public readonly id: string;
8 public readonly name: string;
9}

動作確認

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

これで仮のAPIは一旦終了です。

1$ curl -X GET http://localhost:8080/example
2get example all
3
4$ curl -X GET http://localhost:8080/example/1
5get example 1
6
7$ curl -X POST http://localhost:8080/example -d 'name=TEST'
8create example TEST
9
10$ curl -X PATCH http://localhost:8080/example/1 -d 'name=TEST'
11update example TEST
12
13$ curl -X DELETE http://localhost:8080/example/1
14delete example 1

MikroORMの設定

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

MikroORMの準備

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

src/mikro-orm.config.ts
1import { Logger } from '@nestjs/common';
2import { Options } from '@mikro-orm/core';
3import { PostgreSqlDriver } from '@mikro-orm/postgresql';
4import { TsMorphMetadataProvider } from '@mikro-orm/reflection';
5import { SqlHighlighter } from '@mikro-orm/sql-highlighter';
6
7const logger = new Logger('MikroORM');
8
9const config: Options<PostgreSqlDriver> = {
10 driver: PostgreSqlDriver,
11 metadataProvider: TsMorphMetadataProvider,
12 highlighter: new SqlHighlighter(),
13 debug: true,
14 logger: logger.log.bind(logger),
15 entities: ['dist/**/*.entity.js'],
16 entitiesTs: ['src/**/*.entity.ts'],
17 baseDir: process.cwd(),
18};
19
20export default config;

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

src/app.module.ts
1import { MikroOrmModule } from '@mikro-orm/nestjs';
2import { Module } from '@nestjs/common';
3import { AppController } from './app.controller';
4import { AppService } from './app.service';
5import { ExampleModule } from './example/example.module';
6import config from './mikro-orm.config';
7
8
9@Module({
10 imports: [MikroOrmModule.forRoot(config), ExampleModule],
11 controllers: [AppController],
12 providers: [AppService],
13})
14export class AppModule {}

環境変数の設定

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

src/app.module.ts
1import { MikroOrmModule } from '@mikro-orm/nestjs';
2import { Module } from '@nestjs/common';
3import { ConfigModule } from '@nestjs/config';
4import { AppController } from './app.controller';
5import { AppService } from './app.service';
6import { ExampleModule } from './example/example.module';
7import config from './mikro-orm.config';
8
9@Module({
10 imports: [
11 MikroOrmModule.forRoot(config),
12 ConfigModule.forRoot({
13 isGlobal: true,
14 }),
15 ExampleModule,
16 ],
17 controllers: [AppController],
18 providers: [AppService],
19})
20export class AppModule {}

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

./.env
1MIKRO_ORM_DB_NAME=example
2MIKRO_ORM_PORT=5432

package.jsonの修正

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

package.json
1{
2 /*...*/
3 "mikro-orm": {
4 "useTsNode": true,
5 "configPaths": [
6 "./src/mikro-orm.config.ts",
7 "./dist/mikro-orm.config.js"
8 ]
9 }
10}

Entityの作成

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

./src/example/example.entity.ts
1import { BigIntType, Entity, PrimaryKey, Property } from '@mikro-orm/core';
2
3@Entity()
4export class Example {
5 @PrimaryKey({ type: BigIntType })
6 id: string;
7
8 @Property()
9 name!: string;
10}

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

マイグレーションの設定

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

Configファイルの更新

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

src/mikro-orm.config.ts
1import { Logger } from '@nestjs/common';
2import { Options } from '@mikro-orm/core';
3import { PostgreSqlDriver } from '@mikro-orm/postgresql';
4import { TsMorphMetadataProvider } from '@mikro-orm/reflection';
5import { SqlHighlighter } from '@mikro-orm/sql-highlighter';
6
7const logger = new Logger('MikroORM');
8
9const config: Options<PostgreSqlDriver> = {
10 driver: PostgreSqlDriver,
11 metadataProvider: TsMorphMetadataProvider,
12 highlighter: new SqlHighlighter(),
13 debug: true,
14 logger: logger.log.bind(logger),
15 entities: ['dist/**/*.entity.js'],
16 entitiesTs: ['src/**/*.entity.ts'],
17 baseDir: process.cwd(),
18 migrations: {
19 tableName: 'schema_version',
20 path: './src/migrations',
21 pattern: /^[\w-]+\d+\.ts$/,
22 transactional: true,
23 disableForeignKeys: true,
24 allOrNothing: true,
25 dropTables: true,
26 safe: false,
27 emit: 'ts',
28 },
29 forceUtcTimezone: true,
30};
31
32export default config;
33

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

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

1npx mikro-orm migration:create

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

1src/migrations/Migration20210925112002.ts

マイグレーションの実行

最後にアプリケーション実行時にマイグレーションが実行されるようにします。

MikroORM CLIで実行は出来るのですが、公式が推奨していないので以下のようなスクリプトを用意します。

1import { MikroORM } from '@mikro-orm/core';
2import config from './mikro-orm.config';
3
4(async () => {
5 const orm = await MikroORM.init(config);
6
7 const migrator = orm.getMigrator();
8 await migrator.up();
9 await orm.close(true);
10})();

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

1{
2 /*...*/
3 "scripts": {
4 "start": "yarn migration:dev && nest start",
5 "start:dev": "yarn migration:dev && nest start --watch",
6 "start:debug": "yarn migration:dev && nest start --debug --watch",
7 "start:prod": "yarn migration:prod && node dist/main",
8 "migration:create": "npx mikro-orm migration:create",
9 "migration:dev": "ts-node src/migration",
10 "migration:test": "ts-node src/migration",
11 "migration:prod": "node dist/migration",
12 }
13}
14

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

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

1$ psql example
2
3example=# \d
4                    List of relations
5 Schema |         Name          |   Type   |    Owner    
6--------+-----------------------+----------+-------------
7 public | example               | table    | owner
8 public | example_id_seq        | sequence | owner
9 public | schema_version        | table    | owner
10 public | schema_version_id_seq | sequence | owner
11(4 rows)
12
13example=# \d example
14                                    Table "public.example"
15 Column |          Type          | Collation | Nullable |               Default               
16--------+------------------------+-----------+----------+-------------------------------------
17 id     | bigint                 |           | not null | nextval('example_id_seq'::regclass)
18 name   | character varying(255) |           | not null | 
19Indexes:
20    "example_pkey" PRIMARY KEY, btree (id)
21

サービスを追加

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

サービスの作成

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

1nest g service example --no-spec

サービスの修正

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

src/example/example.service.ts
1import { InjectRepository } from '@mikro-orm/nestjs';
2import { Injectable, NotFoundException } from '@nestjs/common';
3import { EntityRepository, wrap } from 'mikro-orm';
4import { CreateExampleRequestDto } from './dto/create-example-request.dto';
5import { CreateExampleResponseDto } from './dto/create-example-response.dto';
6import { GetExampleResponseDto } from './dto/get-example-response.dto';
7import { UpdateExampleRequestDto } from './dto/update-example-request.dto';
8import { UpdateExampleResponseDto } from './dto/update-example-response.dto';
9import { Example } from './example.entity';
10
11@Injectable()
12export class ExampleService {
13 constructor(
14 @InjectRepository(Example)
15 private readonly exampleRepository: EntityRepository<Example>,
16 ) {}
17
18 public async create(dto: CreateExampleRequestDto) {
19 const { name } = dto;
20 const example = new Example();
21 example.name = name;
22 await this.exampleRepository.persistAndFlush(example);
23 return new CreateExampleResponseDto(example);
24 }
25
26 public async findOne(id: string) {
27 const example = await this.exampleRepository.findOne(id);
28 if (!example) throw new NotFoundException();
29
30 return new GetExampleResponseDto(example);
31 }
32
33 public async findAll() {
34 const examples = await this.exampleRepository.findAll();
35 return examples.map((example) => new GetExampleResponseDto(example));
36 }
37
38 public async update(id: string, dto: UpdateExampleRequestDto) {
39 const example = await this.exampleRepository.findOne(id);
40 const nextExample = { ...example, name: dto.name };
41 wrap(example).assign(nextExample);
42 await this.exampleRepository.flush();
43
44 return new UpdateExampleResponseDto(nextExample);
45 }
46
47 public async remove(id: string) {
48 const example = await this.exampleRepository.findOne(id);
49 await this.exampleRepository.removeAndFlush(example);
50 }
51}
52

src/example/dto/create-example-response.dto.ts
1export type CreateExampleResponse = {
2 id: string;
3 name: string;
4};
5
6export class CreateExampleResponseDto implements CreateExampleResponse {
7 public readonly id: string;
8 public readonly name: string;
9
10 public constructor(object: CreateExampleResponse) {
11 Object.assign(this, object);
12 }
13}
14

src/example/dto/update-example-response.dto.ts
1export type UpdateExampleResponse = {
2 id: string;
3 name: string;
4};
5
6export class UpdateExampleResponseDto implements UpdateExampleResponse {
7 public readonly id: string;
8 public readonly name: string;
9
10 public constructor(object: UpdateExampleResponse) {
11 Object.assign(this, object);
12 }
13}
14

src/example/dto/get-example-response.dto.ts
1import { Example } from '../example.entity';
2
3export type GetExampleResponse = {
4 id: string;
5 name: string;
6};
7
8export class GetExampleResponseDto implements GetExampleResponse {
9 public readonly id: string;
10 public readonly name: string;
11
12 public constructor(object: Example) {
13 Object.assign(this, object);
14 }
15}
16

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

./src/example/example.module.ts
1import { MikroOrmModule } from '@mikro-orm/nestjs';
2import { Module } from '@nestjs/common';
3import { ExampleController } from './example.controller';
4import { Example } from './example.entity';
5import { ExampleService } from './example.service';
6
7@Module({
8 imports: [MikroOrmModule.forFeature({ entities: [Example] })],
9 controllers: [ExampleController],
10 providers: [ExampleService],
11})
12export class ExampleModule {}
13

仮のAPIをCRUDのAPIに修正

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

./src/api/example/example.controller.ts
1import {
2 Body,
3 Controller,
4 Delete,
5 Get,
6 Param,
7 Patch,
8 Post,
9} from '@nestjs/common';
10import { CreateExampleRequestDto } from './dto/create-example-request.dto';
11import { UpdateExampleRequestDto } from './dto/update-example-request.dto';
12import { ExampleService } from './example.service';
13
14@Controller('example')
15export class ExampleController {
16 constructor(private readonly exampleService: ExampleService) {}
17
18 @Post()
19 async create(@Body() dto: CreateExampleRequestDto) {
20 return await this.exampleService.create(dto);
21 }
22
23 @Get(':id')
24 async findOne(@Param('id') id: string) {
25 return await this.exampleService.findOne(id);
26 }
27
28 @Get('')
29 async findAll() {
30 return await this.exampleService.findAll();
31 }
32
33 @Patch(':id')
34 update(@Param('id') id: string, @Body() dto: UpdateExampleRequestDto) {
35 return this.exampleService.update(id, dto);
36 }
37
38 @Delete(':id')
39 remove(@Param('id') id: string) {
40 return this.exampleService.remove(id);
41 }
42}
43

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

1$ curl -X POST http://localhost:8080/example -d 'name=TEST'
2{"name":"TEST","id":"1"}  
3
4$ curl -X POST http://localhost:8080/example -d 'name=TEST'
5{"name":"TEST","id":"2"}  
6
7$ curl -X GET http://localhost:8080/example
8[{"id":"1","name":"TEST"},{"id":"2","name":"TEST"}]
9
10$ curl -X GET http://localhost:8080/example/1
11{"id":"1","name":"TEST"}
12
13$ curl -X PATCH http://localhost:8080/example/1 -d 'name=TEST2'
14{"id":"1","name":"TEST2"}
15
16$ curl -X DELETE http://localhost:8080/example/1
17$ curl -X GET http://localhost:8080/example
18[{"id":"2","name":"TEST"}]

さいごに

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