1
/
5

NestJSでREST(その4)

前回まで

NestJSでREST(その3) | 株式会社ROBON
前回までTypeORMとPostgresの設定ができました。👏TypeORMを使って、モジュールを完成させるNestJSは、DIなので依存関係はうるさくないのですが、下からやります。(実は、OR...
https://www.wantedly.com/companies/company_5181558/post_articles/897244?status=success

一旦、動きました。👏

仕上げ

設定が丸見え

Node.jsのプロジェクトでよくあるような.envにします。

DB_HOST=host

DB_USER=user

DB_PASS=pass​

@nestjs/configの導入と設定

$ npm i --save @nestjs/config

インストールできたら、app.modules.tsを変更してお見せできる状態にします。

import { Module } from '@nestjs/common';

import { ConfigModule, ConfigService } from '@nestjs/config';

import { TypeOrmModule } from '@nestjs/typeorm';

import { AppController } from './app.controller';

import { AppService } from './app.service';

import { CustomersModule } from './customers/customers.module';

import { Customer } from './customers/entities/customer.entity';



@Module({

  imports: [

    ConfigModule.forRoot({

      envFilePath: '.env.dev',

    }),

    TypeOrmModule.forRootAsync({

      imports: [ConfigModule],

      useFactory: (configService: ConfigService) => ({

        type: 'postgres',

        host: configService.get('DB_HOST'),

        port: 5432,

        username: configService.get('DB_USER'),

        password: configService.get('DB_PASS'),

        database: 'postgres',

        entities: [Customer],

        logging: true,

        synchronize: false,

      }),

      inject: [ConfigService],

    }),

    CustomersModule,

  ],

  controllers: [AppController],

  providers: [AppService],

})

export class AppModule {}

以下のドキュメントに説明がありますが、forRootからforRootAsyncに変更しています。

Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Re
https://docs.nestjs.com/techniques/database

最初にやりたかったこと

最初にやりたかった全体像は、その1を参照ください。


NestJSでREST(その1) | 株式会社ROBON
はじめに当社では、クラウドネイティブなSaaSの開発をしており、いつもはWebAPIの実装ならサーバーレスで。となるのですが、今回は、サーバーレスでないWebAPIの実現方法として、NestJS...
https://www.wantedly.com/companies/company_5181558/post_articles/897233

productsの追加

独立エンティティ単体なので、customersと同様に作るだけです。

ordersの追加

まず、Entiryですが、OneToManyとManyToOneの関連を作成しました。(あとで、いろいろうまくいかなくて変更するのですが、この指定は様々なバリエーションを試しました。)


import {

  Entity,

  Column,

  PrimaryColumn,

  OneToMany,

  ManyToOne,

  JoinColumn,

} from 'typeorm';



@Entity({ name: 'order_header' })

export class Order {

  @PrimaryColumn({ name: 'order_id' })

  orderId: number;



  @Column({ name: 'customer_id' })

  customerId: number;



  @Column({ name: 'order_date' })

  orderDate: string;



  @OneToMany(() => OrderDetail, (detail) => detail.header, {

    eager: true,

    cascade: true,

  })

  details: OrderDetail[];

}



@Entity({ name: 'order_detail' })

export class OrderDetail {

  @PrimaryColumn({ name: 'order_id' })

  orderId: number;



  @PrimaryColumn({ name: 'row_number' })

  rowNumber: number;



  @Column({ name: 'product_id' })

  productId: number;



  @Column()

  quantity: number;



  @Column({ name: 'price_per_unit' })

  pricePerUnit: number;



  @ManyToOne(() => Order, (header) => header.details)

  @JoinColumn({ name: 'order_id' })

  header?: Order;

}

そして、Serviceは、updateをsaveに、deleteをremoveに変更しました。これで一見すると動くようになりました。

import {

  Injectable,

  NotFoundException,

  InternalServerErrorException,

} from '@nestjs/common';

import { InjectRepository } from '@nestjs/typeorm';

import { Repository } from 'typeorm';

import { CreateOrderDto } from './dto/create-order.dto';

import { UpdateOrderDto } from './dto/update-order.dto';

import { Order } from './entities/order.entity';



@Injectable()

export class OrdersService {

  constructor(

    @InjectRepository(Order)

    private ordersRepository: Repository<Order>,

  ) {}



  async create(createOrderDto: CreateOrderDto): Promise<Order> {

    return await this.ordersRepository.save(createOrderDto).catch((e) => {

      throw new InternalServerErrorException(e.message);

    });

  }



  async findAll(): Promise<Order[]> {

    return await this.ordersRepository.find({}).catch((e) => {

      throw new InternalServerErrorException(e.message);

    });

  }



  async findOne(id: number): Promise<Order> {

    const order = await this.ordersRepository.findOneBy({

      orderId: id,

    });

    if (!order) {

      throw new NotFoundException(`Order not found (${id})`);

    }

    return order;

  }



  async update(id: number, updateOrderDto: UpdateOrderDto): Promise<Order> {

    return await this.ordersRepository.save(updateOrderDto).catch((e) => {

      throw new InternalServerErrorException(e.message);

    });

  }



  async remove(id: number): Promise<void> {

    const order = await this.ordersRepository.findOneBy({

      orderId: id,

    });

    if (!order) {

      throw new NotFoundException(`Order not found (${id})`);

    }

    await this.ordersRepository.remove([order]);

    return;

  }

}

logging: trueで動かしていたので、removeでorder_detailテーブルのDELETE文が実行されていないことに気がつきました。このため、cascadeオプションを中心に様々なバリエーションを試したのですが、うまく行きませんでした。
(なにか方法をご存知の方おしえてください。ただし、onDelete: 'CASCADE'でDBにやらせるのはナシです)

仕方ないので、Transactionを使って明示的にremoveしてみました。

@@ -51,7 +52,20 @@ export class OrdersService {

     if (!order) {

       throw new NotFoundException(`Order not found (${id})`);

     }

-    await this.ordersRepository.remove([order]);

+

+    const queryRunner = this.dataSource.createQueryRunner();

+    await queryRunner.connect();

+    await queryRunner.startTransaction();

+    try {

+      await queryRunner.manager.remove(order.details);

+      await queryRunner.manager.remove(order);

+      await queryRunner.commitTransaction();

+    } catch (e) {

+      await queryRunner.rollbackTransaction();

+      throw new InternalServerErrorException(e.message);

+    } finally {

+      await queryRunner.release();

+    }

     return;

   }

 }

これで、消えるのですが、今度は無駄なSELECT文が呼び出されていることに気づきます。このため、removeをdeleteに戻して、以下を最終形にしました。

@@ -45,20 +45,14 @@ export class OrdersService {

     });

   }

 

-  async remove(id: number): Promise<void> {

-    const order = await this.ordersRepository.findOneBy({

-      orderId: id,

-    });

-    if (!order) {

-      throw new NotFoundException(`Order not found (${id})`);

-    }

-

+  async remove(id: number): Promise<DeleteResult> {

     const queryRunner = this.dataSource.createQueryRunner();

     await queryRunner.connect();

     await queryRunner.startTransaction();

+    let result: DeleteResult;

     try {

-      await queryRunner.manager.remove(order.details);

-      await queryRunner.manager.remove(order);

+      await queryRunner.manager.delete(OrderDetail, { orderId: id });

+      result = await queryRunner.manager.delete(Order, { orderId: id });

       await queryRunner.commitTransaction();

     } catch (e) {

       await queryRunner.rollbackTransaction();

@@ -66,6 +60,9 @@ export class OrdersService {

     } finally {

       await queryRunner.release();

     }

-    return;

+    if (!result.affected) {

+      throw new NotFoundException(`Order not found (${id})`);

+    }

+    return result;

   }

 }

最後に

今回のソースコードは、以下のリポジトリで公開しています。

GitHub - take0a/nestjs-sample: REST API Sample with NestJS
REST API Sample with NestJS. Contribute to take0a/nestjs-sample development by creating an account on GitHub.
https://github.com/take0a/nestjs-sample


Invitation from 株式会社ROBON
If this story triggered your interest, have a chat with the team?
株式会社ROBON's job postings

TypeScript

Weekly ranking

Show other rankings
Like 荒木 岳夫's Story
Let 荒木 岳夫's company know you're interested in their content