NestJSにMikroORMの導入手順。CRUDのAPIを実装するまで。
バックエンドおはこんにちばんは。最近、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.ts
にConfigModule
を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
の導入手順についてご紹介しました。Prisma
やTypeORM
に比べてシンプルかつトランザクションなど色々と扱いやすかったです。日本では使われているケースをあまり見ないですが、選択の一つとして是非試してみください!