🦁
NestJSでREST(その4)
前回まで
一旦、動きました。👏
仕上げ
設定が丸見え
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に変更しています。
最初にやりたかったこと
最初にやりたかった全体像は、その1を参照ください。
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;
}
}
最後に
今回のソースコードは、以下のリポジトリで公開しています。
GitHubで編集を提案
Discussion