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

Weekly ranking

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