LoginSignup
8
5

More than 3 years have passed since last update.

NestでTypeORM環境を構築してサンプル実装(CRUD+トランザクション+ロック)してみる

Last updated at Posted at 2020-06-28

背景

前回は純粋なTypeORM環境をCLIで構築してみましたが、TypeORMはNestというフレームワークのORMとして利用されているとのことを知ったので、今度はNestでTypeORM環境を構築してみようと思う。
https://qiita.com/yusuke-ka/items/195e6bba4f21a659b424

Nest環境の構築

前回同様、DBは以前インストールしたpostgresql(windows)を利用する。
https://qiita.com/yusuke-ka/items/448843020c0406363ba5#%E6%BA%96%E5%82%99

pgadmin4でデータベースインスタンスだけ作っておく。

image.png

データベース名("nest_typeorm"とした)を入力して作成。

ここからは、コード エディタ(VS Code)上での作業。

諸事情により、まずは作業用のnode環境を作る。

> mkdir work
> cd work

nestのインストールは通常は下記のコマンドで実施する。
(linux環境の場合は下記でやってしまってOK.)

yarn global add @nestjs/cli

本当は↑でインストールしたいところだけど、windowsだとそのままだと使えずパスを通してやる必要があり面倒なので、↓でインストール。

yarn add @nestjs/cli

続いて、nestで新しいプロジェクトを作る。

以下のようにすれば、今いるフォルダを直接プロジェクトのフォルダにできるけど、すでにpackage.jsonが存在していて怒られるので(globalインストールしていればpackage.jsonがないので実行できる)、今いるフォルダは作業用として、下にプロジェクトを作る。

> npx nest new .

↓今回はこっちで実行。

> npx nest new nest

npmかyarnかを聞かれるので、今回はyarnを選択。

yarnでnestコマンドが使えるように、作成されたnestプロジェクト以下のpackage.jsonにscriptを追加しておく。
(npxで実行するのであれば下記の修正は不要だけど、なんとなくyarnでやりたい。)

package.json
  "scripts": {
    "nest" : "nest",
    ...
  },

typeORM関連の依存をインストールしておく。

> yarn add @nestjs/typeorm typeorm pg

最後に設定ファイル(app.module.ts)を更新。

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'nest_typeorm',
      entities: [],
      synchronize: true,
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

デフォルトで、AppControllerやAppServiceなどがimportされているが、これらは最初から入っているサンプルコードなので消してもよい。

今回は使わないので消しておいた。

ついでにsrc以下にある下記のファイルもいらないので削除。
app.controller.ts
app.controller.spec.ts
app.service.ts

これでNest環境のベースは整った。

Nestでサンプル実装してみる

最初にCLIのジェネレータでコードのひな型を作っておく。

> yarn nest g service TestObject
> yarn nest g controller TestObject
> yarn nest g module TestObject

test-objectフォルダが作成され、その下に各ひな形が配置される。

さらに以下のコマンドでモデルのひな形も作成。
(こちらは、明示的にフォルダを指定しないと、「test-object」フォルダに入らなかったので、「test-object/TestObject」のように指定。)

> yarn nest g class test-object/TestObject

test-objectフォルダ以下は下のようになる。

 |- src
   |- test-object
     |- test-object.ts
     |- test-object.controller.ts
     |- test-object.controller.spec.ts
     |- test-object.module.ts
     |- test-object.service.ts
     |- test-object.service.spec.ts
     |- test-object.spec.ts

~.spec.tsが一緒に生成されるがテスト用のコードみたいなので、今回は触れない(今度試してみようと思う)。

また、ジェネレートコマンドを実行すると、app.module.tsが自動的に更新されてしまう。
生成したcontrollerやserviceが自動でimportされるみたい。

今回はモジュール化するためのファイルも作っていて、直接app.module.tsにはimportしなくてよいはずなので、以下のように修正しておく。

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TestObjectModule } from './test-object/test-object.module';
import { TestObject } from './test-object/test-object';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'nest_typeorm',
      entities: [TestObject],
      synchronize: true,
    }),
    TestObjectModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

controllerやserviceは直接ここには書かずに、moduleだけを指定する感じ。
ただ、TypeOrmModuleのentitiesには、モデルオブジェクトを直接書いておかないとコンパイル時にエラーになったので注意。

ここからは中身を実装していく。

とその前に、各ファイルの役割を確認しておく。
サンプルのコードとかを見る限り、なんとなくこんな(↓)感じのイメージかな。

test-object.ts : モデルを定義するファイル
test-object.service.ts : TypeORMの機能を直接使ってDBを操作するファイル
test-object.controller.ts : APIを定義するファイル。serviceの提供するメソッドを呼び出す。
test-object.module.ts : 定義したserviceとかcontrollerとかを一つのモジュールにして提供するファイル。

ということで、まずはモデルファイルを実装。

test-object.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class TestObject {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  attr1: string;
}

シンプルに属性はidとattr1だけ。

続いてサービスの実装。

test-object.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TestObject } from './test-object';

@Injectable()
export class TestObjectService {
  constructor(
    @InjectRepository(TestObject)
    private readonly repository: Repository<TestObject>,
  ) {}

  async all(): Promise<TestObject[]> {
    return this.repository.find();
  }

  async one(id: number): Promise<TestObject> {
    return this.repository.findOne(id);
  }

  async create(data: Partial<TestObject>): Promise<TestObject> {
    return this.repository.save(data);
  }

  async update(id: number, data: Partial<TestObject>): Promise<void> {
    const origin = await this.repository.findOne(id);
    const updateData = Object.assign(origin, data); // 上書き
    this.repository.save(updateData);
  }

  async remove(id: number): Promise<void> {
    const obj = await this.repository.findOne(id);
    this.repository.remove(obj);
  }
}

こちらもシンプルなCRUDだけを実装してみました。
(read系は全件取得と単独指定で取得の2つ)

次は、このサービスを呼び出すコントローラの実装。

test-object.controller.ts
import {
  Controller,
  Get,
  Post,
  Param,
  Body,
  Delete,
  HttpCode,
  HttpStatus,
  Put,
} from '@nestjs/common';
import { TestObjectService } from './test-object.service';
import { CreateTestDataDTO, UpdateTestDataDTO } from './test-object.dto';
import { TestObject } from './test-object';

@Controller('test-object')
export class TestObjectController {
  constructor(private readonly service: TestObjectService) {}

  @Get()
  @HttpCode(HttpStatus.OK)
  all(): Promise<TestObject[]> {
    return this.service.all();
  }

  @Get(':id')
  @HttpCode(HttpStatus.OK)
  one(@Param('id') id: number): Promise<TestObject> {
    return this.service.one(id);
  }

  @Post('create')
  @HttpCode(HttpStatus.CREATED)
  async create(
    @Body() createTestDataDto: CreateTestDataDTO,
  ): Promise<TestObject> {
    return this.service.create(createTestDataDto);
  }

  @Put('update/:id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async update(
    @Param('id') id: number,
    @Body() updateTestDataDto: UpdateTestDataDTO,
  ): Promise<void> {
    this.service.update(id, updateTestDataDto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async remove(@Param('id') id: number): Promise<void> {
    this.service.remove(id);
  }
}

こちらもCRUD操作のAPIを定義。HTTPリクエストをここで処理している感じですね。

更新系の操作は引数にData Transfer Object(DTO)を渡している、これは、src/test-objectの下にtest-object.dto.tsとして別途定義した。

test-object.dto.ts
export class CreateTestDataDTO {
  attr1: string;
}

export class UpdateTestDataDTO {
  attr1: string;
}

最後に、serviceやcontrollerをモジュール化するファイルの実装。

test-object.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TestObjectService } from './test-object.service';
import { TestObjectController } from './test-object.controller';
import { TestObject } from './test-object';

@Module({
  imports: [TypeOrmModule.forFeature([TestObject])],
  exports: [TypeOrmModule],
  providers: [TestObjectService],
  controllers: [TestObjectController],
})
export class TestObjectModule {}

このモジュールを大元のapp.module.tsでimportする感じになっている。

実行

起動して確認してみる。

> yarn start:dev

確認はいつもの「Advanced REST client」。
http://localhost:3000/test-objectに各種リクエストを送ってみる。

説明は省略(全部問題なく動きました。)。

作成

image.png

全件取得

image.png

更新

image.png

1件取得

image.png

削除

image.png

トランザクション管理

ついでにトランザクション管理の実装がどうなるかも試してみる。

test-object.service.tsで更新の処理をトランザクション管理するように変更。

test-object.service.ts
...
import { Repository, Connection } from 'typeorm';
...

@Injectable()
export class TestObjectService {
  constructor(
    ...
    @InjectConnection()
    private readonly connection: Connection,
  ) {}

  ...

  async update(id: number, data: Partial<TestObject>): Promise<void> {
    const queryRunner = this.connection.createQueryRunner();

    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      const origin = await queryRunner.manager.findOne(TestObject, id);
      const updateData = Object.assign(origin, data); // 上書き
      await queryRunner.manager.save(updateData);
      await queryRunner.commitTransaction();
    } catch (err) {
      console.log(err);
      await queryRunner.rollbackTransaction();
    } finally {
      await queryRunner.release();
    }
  }

  ...
}

typeORMのConnectionをimportするようにして、コンストラクタで注入。

更新処理のメソッドの中で、connectionから取得したqueryRunnerで、接続、トランザクションの開始、コミット、リリースといった処理を記述する(try-finallyを使う)。

あとは、repository.findOne(...)とかrepository.save(...)で呼び出していた部分を、
queryRunner.manager.findOne(...)とかqueryRunner.manager.save(...)のような感じで呼び出すように変更するだけ。

注意点としては、repository.findOne(...)とqueryRunner.manager.findOne(...)で引数が違っていること。
repositoryはどのモデルか知っているのに対し、connectionは知らないので、第一引数でモデルを渡してやる必要がある。

トランザクション管理については、他にもやり方はあるみたいだけど、自分にはこのやり方が一番安心できる。

続いて、ロック(SELECT... FOR UPDATE)も試してみる。

上でトランザクション管理するように書き換えた更新の処理で、更新対象のオブジェクトを取得してくる部分をロックしながら取得するように変えてみる。

test-object.service.ts
  ...
  async update(id: number, data: Partial<TestObject>): Promise<void> {
    ...
    try {
      const origin = await queryRunner.manager
        .getRepository(TestObject)
        .createQueryBuilder('test-object')
        .useTransaction(true)
        .setLock('pessimistic_write')
        .where('test-object.id = :id', { id: id })
        .getOne();
      ...
    } catch (err) {
      ...
    } finally {
      ...
    }
  }
  ...
}

今のところ、QueryBuilderで実装する必要があるようなので若干面倒だけど、上記のようにすればロックできそう。

エラーにならないかだけ、動作確認しておく。

更新

image.png

取得して確認

image.png

ちゃんとロックできているかは確認していないが、エラーなくデータが更新されることは確認できた。

一応、SQLのログを出力してみると、"FOR UPDATE"が付いていた。

image.png

ちなみに、SQLのログを出力する設定は下記。

app.module.ts
...
@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...
      logging: true,
    }),
  ],
  ...
})
...

さいごに

今回はNestでTypeORM環境を構築してサンプル実装してみた。

前回、素のTypeORM環境をCLIで作成したときよりは時間がかかったけど、構造がなんとなく理解できたら、すんなりとコードを書くことができた。

そこそこの規模のコードを書くなら、こういったフレームワークを利用して書いた方が、コードを整理できて良さそうですね。テストのフレームワークも付いてるみたいだし。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5