2024.10.08
TypeORMのv0.2からv0.3への移行
こんにちは。次世代システム研究室のL.W.です。
NestJS + TypeORMのプロジェクトの中でTypeORMのバージョンアップを行いましたので、その知見を共有させて頂きたくと思います。
1.前提
TypeORMは0.2.45 (2022-03-04)から0.3.0 (2022-03-17)にされた時、breaking changesが多くがあります。
単にここでのリリースノートに従って、対応するのは物足りないですが、記載されていないところも改修しないと、予想外な結果と繋がる恐れがあります。
こちらの場合、Nodejsのバージョンはv14からv20まで、TypeORMはv0.2からv0.3.20までにバージョンアップを行いました。
2.対応内容
2.1. DBへのコネクションの接続方法はConnectionからDataSourceへ変更
これで、Connection(), createConnection(), getConnectionManager().create(), getConnection, getConnectionOptionsなどが使えなくなります。
v0.2
// ConnectionFactoryの定義ファイル import { Connection, getConnection } from 'typeorm'; @Injectable() export class ConnectionFactory { ... ... constructor(private readonly connectionName: string) { super(); } protected async getdb(): Promise<Connection> { return getConnection(connectionName); } } // ConnectionFactoryModuleの定義ファイル import { ConnectionFactory } from '<上のファイル>'; @Global() @Module({ providers: [ { provide: 'readerConnectionFactory', useFactory: () => new ConnectionFactory('reader'), }, { provide: 'writerConnectionFactory', useFactory: () => new ConnectionFactory('writer'), }, // other connections ... ... ], exports: [ ... ... }) export class ConnectionFactoryModule {} // main関数のModule import { ConnectionFactoryModule } from '<上のファイル>' import { getConnectionOptions } from 'typeorm'; ... ... @Module({ ... ... imports: [ ConnectionFactoryModule, TypeOrmModule.forRootAsync({ useFactory: async () => getConnectionOptions(< connectionName in your ormconfig configuration file >), ... ... ], ... ...
v0.3
「Connection was renamed to DataSource」とリリースノードで書いてあるが、単にConnectionをDataSourceに置き換えるのはダメらしいですね。
Connectionはormconfig configurationファイル(ormconfig
extensions(e.g. json
, js
, ts
, yaml
, xml
, env
)。 一般的にormconfig.jsonになる)からdbセッティング情報を取得していますが、「drop ormconfig
support. ormconfig
still works if you use deprecated methods,however we do not recommend using it anymore, because it’s support will be completely dropped in 0.4.0
.」からすると、ormconfig のサポートが廃止となります。
ただ、0.4.0まで以下のように使えますが、DataSourceのためのインスタンスを新たなファイルで定義するの最善策だと思います。
import ormconfig from "./ormconfig.json" const MyDataSource = new DataSource(ormconfig) // load entities, establish db connection, sync schema, etc. await MyDataSource.connect()
ormconfig.jsonの定義の例
const baseDbConfig = { type: 'mysql', port: 3306, username: <your db name>, password: <your db password>, entities: ['<your dist>/**/*.model{.ts,.js}'], synchronize: <true or false>, migrationsTableName: 'migrations', migrations: ['<your dist>/migrations/*{.ts,.js}'], cli: { migrationsDir: 'src/migrations', }, namingStrategy: <your namingStrategy definitions> connectionTimeout: 123, logging:<true or false>, }; module.exports = [ { // your first db connection name: 'writer', host: <your host>, database: <your db>, extra: { connectionLimit: 123 }, ...baseDbConfig, }, { // your second db connection name: 'reader', host: <your host>, database: <your db>, extra: { connectionLimit: 123 }, ...baseDbConfig, }, ... ... ];
ormconfig.jsonでの情報をdata-source-config.ts(フィアル名が自由です)に移して、定義し直します。
import { DataSource, DataSourceOptions } from 'typeorm'; const baseDbConfig = { type: 'mysql', port: 3306, username: <your db name>, password: <your db password>, entities: ['<your dist>/**/*.model{.ts,.js}'], synchronize: <true or false>, migrationsTableName: 'migrations', migrations: ['<your dist>/migrations/*{.ts,.js}'], cli: { migrationsDir: 'src/migrations', }, namingStrategy: <your namingStrategy definitions> connectionTimeout: 123, logging:<true or false>, }; const writerDbConfig: DataSourceOptions = { // your first db connection name: 'writer', host: <your host>, database: <your db>, extra: { connectionLimit: 123 }, ...baseDbConfig, }; const readerDbConfig: DataSourceOptions = { // your second db connection name: 'reader', host: <your host>, database: <your db>, extra: { connectionLimit: 123 }, ...baseDbConfig, }; // DataSourceを定義する export const writerDataSource = new DataSource(writerDbConfig); export const readerDataSource = new DataSource(readerDbConfig);
これで、上のv0.2のコードを以下のように改修する。
// ConnectionFactoryの定義ファイル //import { Connection, getConnection } from 'typeorm'; import { readerDataSource, writerDataSource, } from '<your data source config file>'; import { DataSource } from 'typeorm'; @Injectable() export class ConnectionFactory { ... ... constructor(private readonly connectionName: string) { super(); } protected async getdb(): Promise { return this.connectionName === 'reader' ? readerDataSource : writerDataSource; } } // ConnectionFactoryModuleの定義ファイル import { ConnectionFactory } from '<上のファイル>'; @Global() @Module({ providers: [ { provide: 'readerConnectionFactory', useFactory: () => new ConnectionFactory('reader'), }, { provide: 'writerConnectionFactory', useFactory: () => new ConnectionFactory('writer'), }, // other connections ... ... ], exports: [ ... ... }) export class ConnectionFactoryModule {} // main関数のModule import { ConnectionFactoryModule } from '<上のファイル>' //import { getConnectionOptions } from 'typeorm'; import { writerDataSource, readerDataSource } from '< your data-source config file>'; ... ... @Module({ ... ... imports: [ ConnectionFactoryModule, TypeOrmModule.forRootAsync({ name: 'writer', useFactory: async () => writerDataSource.options, dataSourceFactory: async (options) => { writerDataSource.setOptions(options); if (!writerDataSource.isInitialized) { await writerDataSource.initialize(); } return writerDataSource; }, }), TypeOrmModule.forRootAsync({ name: 'reader', useFactory: async () => readerDataSource.options, dataSourceFactory: async (options) => { readerDataSource.setOptions(options); if (!readerDataSource.isInitialized) { await readerDataSource.initialize(); } return readerDataSource; }, }), ... ... ], ... ...
2.2. find*
and count*
方法の使い方が変更されました。
2.2.1 findOne
findOne(), findOne(id)が廃棄されました。「findOne
and QueryBuilder.getOne()
now return null
instead of undefined
in the case if it didn’t find anything in the database」これも要注意。
v0.2
// DataSource.createQueryRunner().manager.findOneの使い方 const user = await DataSource.createQueryRunner().manager.findOne( User, id, { relations: ['photos'], }, ); // Repository.findOneの使い方 @InjectRepository(User) private readonly userRepository: Repository<User>, await this.userRepository.findOne(id, { relations: ['photos'] }) await this.userRepository.findOne(id)
v0.3
// DataSource.createQueryRunner().manager.findOneの使い方 const user = await DataSource.createQueryRunner().manager.findOne(User, { where: { id: id }, relations: ['photos'], }); // Repository.findOneの使い方 @InjectRepository(User) private readonly userRepository: Repository<User> await this.userRepository.findOne({ where: { id: id }, relations: ['photos'] }) await this.userRepository.findOneBy({id:id})
2.2.2 find
v0.2
await this.userRepository.find({ id: In(ids), deletedAt: null })
v0.3
await this.userRepository.find({ where: { id: In(ids), deletedAt: IsNull() } })
2.2.3 count
v0.2
await this.userRepository.count({deletedAt: null})
v0.3
await this.userRepository.count({ where: { deletedAt: IsNull() }})
2.2.4 FindConditions (where in FindOptions) was renamed to FindOptionsWhere。FindManyOptionsでのorderの使い方が変更されました。
FindOptionsWhereの使い方がFindConditionsのと変わらないです。
FindManyOptionsでのorderの使い方は以下のように変更
const params: FindManyOptions<User> = {}; params.where = {}; // v0.2での使い方 //params.order.updatedAt = 'DESC'; // v0.3での使い方 params.order = { updatedAt: 'DESC' };
2.2.5 null->IsNull()
検索条件では、nullではなく、明確にIsNull()を指定する必要があります。ではないと、予定通りの結果が至らないです。
@InjectDataSource('reader') public readonly dataSource: DataSource, ... ... //const user = await getConnection('reader') //v0.2 const user = await this.dataSource .getRepository(User) .findOne({ // v0.2 //id: id, //deletedAt: null, //v0.3 where: { id: id, deletedAt: IsNull(), }, });
2.2.6 Repository.findOne({ <your property> })で
<your property>がnullまたはundefinedの場合、v0.2ではnullが返却できたが、v0.3の場合、いつも一つのレコードを返却された。
詳しくはここで参照できます。
nameを例にする。
private async hadSavedName(name: string): Promise<boolean> { // const user = await this.userRepository.findOne({ name }); // v0.2ではnameがundefined場合、userがnullとなる。 // v0.3では、以下のように修正、nameのundefinedの判定を避ける。 if (!name) return false; const user = await this.userRepository.findOne({ where: { name } }); // v0.3ではnameがundefinedの場合、一つのuserが返却される。 return !!user; }
2.3. findByIdsが廃棄されました
findByIdsはidだけではなく、PrimaryKeyであれば、PrimaryKeyで検索できる方法です。極めて便利な方法ですが、廃棄されてしまいました。
v0.3ではfindで代替できるが、使い方が要注意。
//v0.2 //const primaryKeys = [1,2,3] //await DataSource.createQueryRunner().manager.getRepository.findByIds(primaryKeys); //v0.3では、複合主キー(composite primary key)も含めて、PrimaryKeyによって、使い分けます。 const primaryColumns = repository.metadata.primaryColumns; const dAOFindManyOptions: FindManyOptions = {}; const repository = await DataSource.createQueryRunner().manager.getRepository(User); const joinedDAO = await repository.find(<findOptions>); if (primaryColumns.length === 1) { const primaryKeyName = primaryColumns[0].propertyName; const primaryKeys = joinedDAO.map((dao) => dao[primaryKeyName]); dAOFindManyOptions.where = { [primaryKeyName]: In(primaryKeys), }; } else { const primaryKeys = joinedDAO.map((dao) => { return primaryColumns.reduce((pk, column) => { pk[column.propertyName] = dao[column.propertyName]; return pk; }, {}); }); dAOFindManyOptions.where = primaryKeys; } const result = await ( await queryRunner.manager.getRepository() ).find(dAOFindManyOptions);
2.4. Repository.createQueryBuilder.whereでのIn操作はv0.2と異なる
v0.2
IN (:…Array), IN (:Array)両方が行けます。想定の通りに結果が出られます。
const names = [ 'Jim','Tom', ]); const users = await this.userRepository .createQueryBuilder('user') //.where('(user.name) IN (:...names)', // -> 行ける .where('((user.name) IN (:names)', {// -> 行ける names: names, }) .getMany();
v0.3の場合、tupleの場合、IN (:…Array)だと、想定の通りに結果が出られないです。
IN (:Array)に置き換える必要があります。
const nameAges = [ ['Jim',33], ['Tom',20], ]); const users = await this.userRepository .createQueryBuilder('user') //.where('(user.name, user.age) IN (:...nameAges)', // -> 結果が出られない。 .where('((user.name, user.age) IN (:nameAges)', {// -> 行ける nameAges: nameAges, }) .getMany();
2.5. Repository.manager.fromでのIn操作はv0.2と異なる
v0.2
@InjectRepository(User) private readonly userRepository: Repository<User>, const row = await this.outgoingRepository.manager .createQueryBuilder() .select('user.*') .from( '(' + ` select users.id from users where users.name IN (:...names) ` + ')', 'otherUsers', ) .setParameters({ names: [ 'Jim', 'Tom' ], }) .getRawMany();
v0.3では、上のコードは想定通りに検索できなくなりました。以下のように改修する必要があります。
@InjectRepository(User) private readonly userRepository: Repository<User>, const row = await this.outgoingRepository.manager .createQueryBuilder() .select('user.*') .from( '(' + ` select users.id from users where -- ここで修正 users.name IN (:names) ` + ')', 'otherUsers', ) .setParameters({ names: [ 'Jim', 'Tom' ], }) .getRawMany();
2.6. 複合主キー(composite primary key)の場合、DataSource.createQueryRunner().manager.updateの使い方が異なる
UserのPrimary Keyが複合主キー(composite primary key)の場合、criteriaがObjectまたはObject[]の場合、更新できなくなります。
const userList = [user1, user2]; const userListIds = userList.map( (u) => u.id, ); const updateParams: Partial<User> = { age: 20 }; await queryRunner.manager.update( User, //userList, //v0.2では行ける。v0.3の場合、更新できない。 { id: In(userListIds) }, //v0.3でこのように修正すれば、更新できる。 updateParams, );
2.7. LessThan、MoreThan、MoreThanOrEqual、LessThanOrEqualの使い方が異なる
BigNumberの属性がある場合、パラメーターをBigNumber化する必要がある。
ではないと、予定通りに検索できないです。
// userの定義 @Entity('user') export class User { @PrimaryGeneratedColumn('increment') id: number; @Column(transformerColumnOptions) salary: BigNumber; ... ... } export const transformerColumnOptions: ColumnOptions = { type: 'decimal', precision: 16, scale: 0, transformer: { from: (v) => f1(v), // value to BigNumber to: (v) => f2(v), // BigNumber to value }, nullable: true, }; //検索ファイル定義の抜粋 import { LessThan, MoreThan MoreThanOrEqual, LessThanOrEqual, } from 'typeorm'; import BigNumber from 'bignumber.js'; protected setFindOption( ): FindManyOptions<User> { const params: FindManyOptions<User> = {}; params.where = { deletedAt: IsNull(), }; if (input.isPaid) { // params.where.salary = MoreThan(0); // v0.2では検索できる。v0.3では想定通りに検索できない params.where.salary = MoreThan(new BigNumber(0)); // v0.3ではMoreThanのパラメーターをBigNumber化 } ... ... return params; }
2.8. マイグレーション
TypeORMのv0.3でのマイグレーションがv0.2のと変更されました。
package.josn
"migrate": "ts-node node_modules/typeorm/cli.js migration:run -d ./src/config/migration-data-source.ts", "migrate:show": "ts-node node_modules/typeorm/cli.js migration:show -d ./src/config/migration-data-source.ts", "migrate:rollback": "ts-node node_modules/typeorm/cli.js migration:revert -d ./src/config/migration-data-source.ts", "generate-migrations": "ts-node node_modules/typeorm/cli.js migration:generate -d ./src/config/migration-data-source.ts",
migration-data-source.tsの定義は以下の通りです。
import { DataSource } from 'typeorm'; import { writerDbConfig } from 'data-source-config.ts'; // DataSourceのために、定義したdbセッティングファイル export default new DataSource(writerDbConfig);
generate-migrationsを利用するとき、出力先を指定すれば、マイグレーションが生成されます。
yarn generate-migrations ./<your migrations distanation>/your_migrations で
[timestamp]-your_migrationsが生成されます。
yarn migrateで実行できます。
select * from migrationsというsqlで確認できます。
3.まとめ
この記事では全ては網羅していないので参考程度に読んでいただければと思います。
何よりも、TypeORMのソースコードを確認しながらバージョンアップを行うことですが、ソースコードのボリュームが大きいので、手間がかかりますね。
このように修正して、問題が解決できたということに止まらなく、これからTypeORMのソースコードを深掘りしようと思います。
4.最後に
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD