🦁

NestJSでREST(その4)

2023/04/14に公開

前回まで

https://zenn.dev/robon/articles/f5e5907aa871a5

一旦、動きました。👏

仕上げ

設定が丸見え

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

DB_HOST=host
DB_USER=user
DB_PASS=pass

@nestjs/configの導入と設定

$ npm i --save @nestjs/config

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

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

https://docs.nestjs.com/techniques/database

最初にやりたかったこと

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

https://zenn.dev/robon/articles/76d4ec767b72ae

productsの追加

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

ordersの追加

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

orders/entities/order.entity.ts
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に変更しました。これで一見すると動くようになりました。

orders/orders.service.ts
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してみました。

orders/orders.service.ts
@@ -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に戻して、以下を最終形にしました。

orders/orders.service.ts
@@ -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;
   }
 }

最後に

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

https://github.com/take0a/nestjs-sample

GitHubで編集を提案
株式会社ROBONの技術ブログ

Discussion