概要
弊社ではNode.jsのサーバーサイドフレームワークとしてNestJSを使用しており、データベースとの統合の部分についてはNestJSで公式にサポートされているTypeORMを使用しています。
そして昨年TypeORM v0.3とNestJS v9がリリースされたため、最新のバージョンに追随しようということでバージョンアップを試みたのですが、特にTypeORMにおいて多くの破壊的変更があり、当初の想定より大きな修正をすることとなりました。
その破壊的変更の中で一番手間がかかり修正範囲が広くなったのがCustomRepositoryの変更でした。v0.2の時はclassで定義するようになっていたのですが、v0.3ではオブジェクトで定義するように変わっています。このような変更があると、NestJSでのDIの方法についても考えないといけないのですが、公式ドキュメントではその方法について特に記載はありませんでした。(2023/05時点)
ということで今回はCustomRepositoryの変更と、NestJSでどのようにDIするかについて重点をおいて解説していきます。
CustomRepositoryの定義方法の変更と問題点
TypeORM v0.2では以下のようにclassを用いてCustomRepository定義していました。
import { Entity, EntityRepository, Repository } from 'typeorm';
@Entity({
name: 'hoge',
})
export class HogeEntity {/*省略*/}
@EntityRepository(HogeEntity)
export class HogeRepository extends Repository<HogeEntity> {
async findHoge(): Promse<HogeEntity> = {/*省略*/}
async findHoges(): Promse<HogeEntity[]> = {/*省略*/}
}
これがv0.3になるとオブジェクトに変更されました。以下のようなかたちになります。
import ormconfig from './ormconfig';
import { DataSource, Entity, EntityRepository, Repository } from 'typeorm';
@Entity({
name: 'hoge',
})
export class HogeEntity {/*省略*/}
const dataSource = new DataSource(ormconfig);
await dataSource.initialize();
export type HogeRepositoryType = {
findHoge(): Promse<HogeEntity>;
findHoges(): Promse<HogeEntity[]>;
}
export const HogeRepository: HogeRepositoryType = dataSource.getRepository(Entity).extends({
findHoge(): Promse<HogeEntity> = {/*省略*/},
findHoges(): Promse<HogeEntity[]> = {/*省略*/},
})
ここで急にDataSourceという概念が出てきましたが、v0.2で言うところのConnectionだと思ってもらえれば大丈夫かと思います。つまりv0.2ではConnectionでデータベースに対して接続していましたが、v0.3はDataSourceを用いて接続することになります。
変更としてはこの通りなのですが、ここで考えないといけなくなるのはDataSourceをどのようにCustomRepositoryに渡すかという問題になります。
例えば弊社の場合では、TypeORMのEntityやRepositoryを記述したgitリポジトリ(modelsと呼ぶことにします)と、そのリポジトリを使うアプリケーションのgitリポジトリ(appと呼ぶことにします)を分割していて、appのpackage.jsonにmodelsを記述することでライブラリとして読み込むようなかたちをとっています。
この場合DataSourceはapp側で生成するのが自然ですし、modelsの各CustomRepositoryでDataSourceを作りdbへのコネクションを大量に作るわけにもいかないです。
よって目指したいのは、TypeORM v0.2の時のようにmodelsでCustomRepositoryを定義して、アプリケーション側のDataSourceを用いてアプリケーション側でCustomRepositoryが生成されるようなかたちとなります。
※ちなみにNestJSを使ってTypeORM v0.2の時のようにCustomRepositoryをclassで扱うような形にもできなくないようですが、その場合modelsがNestJSに依存してしまうため弊社のような場合には目指すべきかたちではないです。
classでCustomRepository
CustomRepositoryの関数化
上記の問題を解決するために今回は以下のようにCustomRepositoryを返す関数を作ることとします。
import { DataSource, Entity, EntityRepository, Repository } from 'typeorm';
@Entity({
name: 'hoge',
})
export class HogeEntity {/*省略*/}
export type HogeRepository = ReturnType<typeofHogeRepositoryFunc>;
export const HogeRepositoryFunc = (dataSource: DataSource) =>
dataSource.getRepository(Entity).extends({
findHoge(): Promse<HogeEntity> = {/*省略*/},
findHoges(): Promse<HogeEntity[]> = {/*省略*/},
})
CustomRepositoryのtypeについてはTypeScriptのReturnTypeを使うと便利です。
このようにすると、appで生成したDataSourceを渡すことで、appとmodelsをきれいに分割することが可能となります。
あと問題になるのはNestJSのDIです。TypeORM v0.2ではCustomRepositoryがClassだったので、そこを工夫していかないといけなくなります。
NestJSでのCustomRepositoryのDI
Module
まずはTypeORM v0.2でのCustomRepositoryのDIについて振り返ります。
v0.2ではmoduleで以下のようにTypeORMModule.forFeatureにCustomRepositoryのclassを渡していました。
@Module({
imports: [
TypeORMModule.forFeature([HogeRepository, HogeEntity]),
],
providers: [HogeService],
controllers: [HogeController],
})
export class HogeModule {}
しかしながら今回はcustomRepositoryの関数をmodelsに定義したため、TypeORMModule.forFeatureは使えません。
そこでprovidersに直接指定することによってDIを実現します。
まずProviderを作る関数を以下のように用意します。
import { Provider } from '@nestjs/common';
import { getCustomRepositoryToken, getDataSourceToken } from '@nestjs/typeorm';
import { DataSource, DataSourceOptions, Repository } from 'typeorm';
export function createTypeORMCustomProviders(
customRepositoryFactories?: ((dataSource: DataSource) => Repository<any> & any)[],
dataSource?: DataSource | DataSourceOptions | string,
): Provider[] {
return (customRepositoryFactories || []).map((customRepositoryFactory) => ({
provide: getCustomRepositoryToken(customRepositoryFactory),
useFactory: customRepositoryFactory,
inject: [getDataSourceToken(dataSource)],
}));
}
これはFactory providersと呼ばれているものの配列を作る関数です。
Factory providersのドキュメント
Factory providersのソースコード
このProviderのprovide, useFactory, injectについてそれぞれ見ていきましょう。
まずprovideについてですが、ここには指定したCustomRepositoryを特定するための一意なトークンを設定する必要があります。
そこでnestjs/typeormにあるgetCustomRepositoryTokenを使いました。
getCustomRepositoryToken
次にuseFactoryですが、ここではインジェクトされるfactory functionを設定します。具体的にはmodelsで作成したHogeRepositoryFuncを設定すれば良いこととなります。
最後にinjectについてですが、nestjs/typeormのgetDataSourceTokenを使用して、dataSourceトークン生成します。
getDataSourceToken
このinjectに与えられたトークンをもとに、内部で生成されたDataSourceがuseFactoryで指定された関数にインジェクションされます。
結果としてmoduleは以下のようになります。
@Module({
imports: [TypeORMModule.forFeature([HogeEntity])],
providers: [
HogeService,
...createTypeORMCustomProviders([HogeRepositoryFunc],
],
controllers: [HogeController],
})
export class HogeModule {}
Service
まずはTypeORM v0.2でのserviceについて見ていきます。
import { InjectRepository } from '@nestjs/typeorm';
export class HogeService {
constructor(
@InjectRepository(HogeRepository) private readonly hogeRepository: HogeRepository,
) {}
}
InjectRepositoryにclassのCustomRepositoryを渡すことでDIを実現していましたが、今回はfactory functionとしたためInjectRepositoryは使えません。そこでInjectRepositoryに変わるデコレーターを以下のように自作します。
import { Inject } from '@nestjs/common';
import { getCustomRepositoryToken } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
export const InjectCustomRepository = (
customRepositoryFactory: (dataSource: DataSource) => Repository<any> & any,
): ReturnType<typeof Inject> => Inject(getCustomRepositoryToken(customRepositoryFactory));
このgetCustomRepositoryTokenによるトークンとmoduleで設定したトークンが一致し、無事インジェクションされることとなります。
これを使って以下のようにserviceを作れば完成です
export class HogeService {
constructor(
@InjectCustomRepository(HogeRepositoryFunc) private readonly hogeRepository: HogeRepository,
) {}
}
まとめ
以上がTypeORM v0.3のcustomRepositoryをNestJSでのDIするための方法の1つとなります。CustomRepository以外にもv0.3は変更が多くあり手間がかかりますが、NestJSのORMというとTypeORMがデフォルトのような扱いになっていますし、うまく付き合っていきましょう。