Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
大規模なORMバージョンアップ作業を 乗り越えた話
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Tech Leverages
PRO
July 01, 2024
6.7k
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
大規模なORMバージョンアップ作業を 乗り越えた話
Tech Leverages
PRO
July 01, 2024
More Decks by Tech Leverages
See All by Tech Leverages
並列化でチームのアウトプットを増やす
leveragestech
PRO
0
28
Engineering ManagerがAI時代に この先生きのこるには?
leveragestech
PRO
1
83
最新技術を"今は選ばない"という技術選定
leveragestech
PRO
0
520
毎⽇dumpされるDBにCDCは無⼒だっ た、、FederatedQueryで繋ぎ直した データ連携の試⾏錯誤
leveragestech
PRO
0
51
Tableauを活かすためにTableauに制約を設けた話
leveragestech
PRO
0
79
営業支援システムと歩んだ7年半の変遷
leveragestech
PRO
0
140
DMBOKを使ってレバレジーズのデータマネジメントを評価した
leveragestech
PRO
0
820
Google ADKのSub Agentを Agentic Workflowに移行し、 遷移成功率を改善した話
leveragestech
PRO
0
18
ハッカソンから社内プロダクトへ AIエージェント ko☆shi 開発で学んだ4つの重要要素
leveragestech
PRO
0
5k
Featured
See All Featured
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
Building an army of robots
kneath
306
46k
Leo the Paperboy
mayatellez
7
1.9k
Test your architecture with Archunit
thirion
1
2.3k
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
450
Are puppies a ranking factor?
jonoalderson
1
3.7k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
Side Projects
sachag
455
43k
How to train your dragon (web standard)
notwaldorf
97
6.7k
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
170
Abbi's Birthday
coloredviolet
3
8.3k
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.6k
Transcript
大規模なORMバージョンアップ作業を 乗り越えた話 NALYSYSグループ 桐生直輝 2024/02/14
自己紹介 桐生直輝 (24歳) • 入社:2023年3月 • 所属:NALYSYSグループ ◦ バックエンド〜インフラ担当 •
趣味:コンピュータいじり、自宅サーバー構築 ◦ おうちKubernetes運用中です ◦ HOMENOC(AS59105)加盟
あらすじ
今回の内容 今日の主役: TypeORM • Node.js用のORマッパーライブラリ • TypeScriptの機能を活用していい感じのDBマッピングができる • NALYSYSのバックエンドはこれにガッツリ依存
今回の内容 One day...
今回の内容 TypeORM作者「メジャーバージョンアップします」
今回の内容 クソデカ破壊的アップデート 参戦! • よく使うグローバル関数の廃止(影響大) • よく使う関数のシグネチャ変更(影響大) • 型で検知しにくい破壊的変更 ◦
undefinedのかわりにnullが返ってくるようになったり • etc 変更箇所のリスト(一部)→ https://blog.open.tokyo.jp/2022/05/04/upgrade-typeorm-0-3.html
今回の内容 前任者 何箇所修正する必要あるんだ・・? 気づかずに壊れる場所とかも出そうだし 絶対無理やんこんなの・・・ 既にコードベースでかいのに勘弁して
今回の内容 そして1年の月日が流れた・・・ (放置)
今回の内容 TypeORMのバージョン 塩漬けなのやばいっすよ ぼく PdM 確かに・・・でもコード多いし 破壊的変更多いしキツくない? 2023年7月
今回の内容 ぼく PdM 自動化とか頑張れば 多分行けるっす マジ?じゃあ一旦やってみて 2023年7月
今回の内容 一ヶ月後・・・
今回の内容 ぼく PdM 一人で完全アップグレードまで いけたっす サンキュー! 2023年8月
今回の内容 クソデカアップデートへの対処法をお話します • TypeORM0.2 -> 0.3移行にて実際に用いた手法をご紹介 ◦ ts-morphを用いたコードの自動書き換え ◦ 移行用ライブラリによる漸進的な移行
◦ 自動テストを活用した影響調査 • 今後のアップデート作業の参考になれば!
ts-morphによる自動コード編集
ts-morphの活用 破壊的変更の一例: findOneメソッド • 引数の形に応じて関数を使い分けるように変更 ◦ whereオプションで条件指定 →そのままでOK ◦ ◦
◦ where条件を直接書いている →findOneByに変更 repo.findOne({ where: { id: someId, }, }); repo.findOne({ where: { id: someId, }, }); repo.findOne({ id: someId, }); repo.findOneBy({ id: someId, });
ts-morphの活用 正規表現でのマッチが難しい! • 場合によって引数の形が多様に変化するため repo.findOne({ relation: ["hoge"], where: { id:
someId, }, }); where以外のオプションが指定されていることがある 中身が更に入れ子になっている可能性がある repo.findOne({ id: someId, rel1: { id: someRel1Id, rel2: { ... }, }, });
今回の内容 💡 ASTを使おう!
ts-morphの活用 普通の文字列置換と何が違うの? • 単純な変更なら、文字列置換でまとめてできる export type SomeType = { ...
oldNameField : string; ... }; const v: SomeType = { oldNameField : 'value', ... }; v.oldNameField = ... /oldNameField/newNameField/ export type SomeType = { ... newNameField : string; ... }; const v: SomeType = { newNameField : 'value', ... }; v.newNameField = ...
ts-morphの活用 ソースコードを ASTとして操作する • AST(Abstract Syntax Tree) = 抽象構文木 •
ソースコードの意味的な構造を取り出したもの • プログラム処理しやすい(コンパイラの内部表現)
ts-morphの活用 ソースコードを ASTとして操作する • AST(Abstract Syntax Tree) = 抽象構文木 {
key1: 'value1', key2: { key3: 'value2' } } ObjectLiteralExpression PropertyAssignment PropertyAssignment StringLiteral value1 Identifier key1 PropertyAssignment ObjectLiteralExpression Identifier key2 StringLiteral value2 Identifier key3
ts-morphの活用 ソースコードを ASTとして操作する • フォーマット等で表現が違っても、 ASTは同じになる ◦ 意味的な操作を扱いやすい ObjectLiteralExpression PropertyAssignment
PropertyAssignment StringLiteral value1 Identifier key1 PropertyAssignment ObjectLiteralExpression Identifier key2 StringLiteral value2 Identifier key3 { key1: 'value1', key2: { key3: 'value2' } } { key1: 'value1', key2: { key3: 'value2', }, } ASTに変換
ts-morphの活用 ソースコードを ASTとして操作する • 例:特定のキーの値を取り出して外側に持ってくる call({ key1: { key2: 'value',
key3: { key4: 'value', }, }, }) call({ key2: 'value', key3: { key4: 'value', }, })
ts-morphの活用 ソースコードを ASTとして操作する • まずはASTの構造を知る ◦ TypeScript AST Viewerで解析可能 call({
key1: { key2: 'value', key3: { key4: 'value', }, }, })
ts-morphの活用 ソースコードを ASTとして操作する • つまり・・・ call({ key1: { key2: 'value',
key3: { key4: 'value', }, }, }) これを上に 持ってくる
ts-morphの活用 ts-morphでのAST操作手順 • プロジェクト(ソースコード)の読み込み • 目的箇所のASTノードの取り出し • AST操作 • 書き出し
※ 細かい話なので、ここでは簡単に流します 詳細はスライドの内容をご確認ください
ts-morphの活用 ts-morphでのAST操作手順 • プロジェクト(ソースコード)の読み込み • 目的箇所のASTノードの取り出し • AST操作 • 書き出し
ts-morphの活用 プロジェクト (ソースコード )の読み込み import { Project } from 'ts-morph';
import path from 'path'; // 初期化 const project = new Project({ tsConfigFilePath: './tsconfig.json', skipAddingFilesFromTsConfig: true, }); // 必要なソースを追加 project.addSourceFilesAtPaths(path.resolve(__dirname, './src/**/*.ts'));
ts-morphの活用 ts-morphでのAST操作手順 • プロジェクト(ソースコード)の読み込み • 目的箇所のASTノードの取り出し • AST操作 • 書き出し
ts-morphの活用 目的箇所の ASTノードの取り出し • 今回の場合 ◦ call関数の定義ノードを取り出す ◦ call関数に対して「Find All
References」を行い参照箇所を取り出す ◦ 関数呼び出しの引数ノードを取り出す
ts-morphの活用 目的箇所の ASTノードの取り出し export function call(obj: object) {} src/call.ts import
{ call } from './call' call({ key1: { key2: 'value', key3: { key4: 'value', }, }, }) src/main.ts
ts-morphの活用 目的箇所の ASTノードの取り出し // call関数の定義を取り出し const callSource = project.getSourceFileOrThrow('src/call.ts'); const
callFunction = callSource.getFunctionOrThrow('call'); // call関数への参照ノードを列挙 for (const ref of callFunction.findReferencesAsNodes()) { // CallExpressionとして使われている箇所を編集 const callExpression = ref.getFirstAncestorByKind(SyntaxKind.CallExpression); if (!callExpression) continue; // 引数を取り出し const arg0 = callExpression.getArguments()[0]; const argAsObj = arg0?.asKind(SyntaxKind.ObjectLiteralExpression); if (!argAsObj) continue; }
ts-morphの活用 目的箇所の ASTノードの取り出し // call関数の定義を取り出し const callSource = project.getSourceFileOrThrow('src/call.ts'); const
callFunction = callSource.getFunctionOrThrow('call'); // call関数への参照ノードを列挙 for (const ref of callFunction.findReferencesAsNodes()) { // CallExpressionとして使われている箇所を編集 const callExpression = ref.getFirstAncestorByKind(SyntaxKind.CallExpression); if (!callExpression) continue; // 引数を取り出し const arg0 = callExpression.getArguments()[0]; const argAsObj = arg0?.asKind(SyntaxKind.ObjectLiteralExpression); if (!argAsObj) continue; } call( { key1: { key2: 'value', key3: { key4: 'value', }, }, } )
ts-morphの活用 ts-morphでのAST操作手順 • プロジェクト(ソースコード)の読み込み • 目的箇所のASTノードの取り出し • AST操作 • 書き出し
ts-morphの活用 AST操作 // key1の中身を取り出す const key1 = argAsObj.getProperty('key1')?.asKind(SyntaxKind.PropertyAssignment); if (!key1)
continue; const key1Value = key1.getInitializerOrThrow().getText(); // 引数の置き換え callExpression.removeArgument(arg0); callExpression.insertArgument(0, key1Value); call({ key1: { key2: 'value', key3: { key4: 'value', }, }, })
ts-morphの活用 AST操作 // key1の中身を取り出す const key1 = argAsObj.getProperty('key1')?.asKind(SyntaxKind.PropertyAssignment); if (!key1)
continue; const key1Value = key1.getInitializerOrThrow().getText(); // 引数の置き換え callExpression.removeArgument(arg0); callExpression.insertArgument(0, key1Value); call({ key1: { key2: 'value', key3: { key4: 'value', }, }, }) call({ key2: 'value', key3: { key4: 'value', }, })
ts-morphの活用 ts-morphでのAST操作手順 • プロジェクト(ソースコード)の読み込み • 目的箇所のASTノードの取り出し • AST操作 • 書き出し
ts-morphの活用 ファイル書き出し • AST操作はメモリ上で行うので、最後に保存する必要がある // ファイルに書き出す project.saveSync();
ts-morphの活用 ts-morphでのAST操作 これで、 • callの呼び出し箇所を全部取り出して • 引数のオブジェクトの一番外側を捨てる をスクリプト化できた! call({ key1:
{ key2: 'value', key3: { key4: 'value', }, }, }) call({ key2: 'value', key3: { key4: 'value', }, })
ts-morphの活用 実際、どれくらい役に立った? • 5000行以上にわたる変更を自動化できた! ◦ 手作業だったら心が死んでいた・・・ • 変更をミスなく行えた ◦ 手作業特有の間違いが発生しないので
• conflictの解消も楽々 ◦ conflictしたら、元のコードを捨てる →スクリプト再適用すればいいだけ
自動化最高!!
ts-morphの活用 自動化の弱点 • 一気に全部変わってしまうのでレビューが大変 • 何も対策しないと0.2 -> 0.3移行のPRがバカでかくなってしまう・・・ → 次の工夫(移行用ライブラリの活用)に繋がります
移行用ライブラリを用いた工夫
漸進的な移行の実現 変更箇所が多すぎるという問題は解決してない! • 1PRで5000箇所もレビューできるわけない • 機能開発が並行してるので、そんな大きい変更を投げたらすぐに conflictしまくる • 問題があったときのロールバックも大変すぎるし・・・ →
0.2を使いつつ、ちょっとずつ 0.3の記法に移行していきたい
漸進的な移行の実現 移行用ライブラリの作成 • TypeORM0.2をフォーク • 0.3にしか存在しないメソッド(findOneBy等)を追加実装 ◦ 破壊的変更のくせに、 findOneBy等の移行先の関数は 0.2には実装されていないため
→ 0.2をベースとして新しい記法への部分的移行が可能に!
漸進的な移行の実現 移行のイメージ 現状 TypeORM 0.2 TypeORM0.2記法 移行中 TypeORM 0.2/0.3 両対応ライブラリ
0.2 記 法 TypeORM0.3 記法 徐々に移行していく 移行完了 TypeORM0.3記法 TypeORM 0.3 ※ 0.2上で模倣できない機能は 最後にまとめて移行
漸進的な移行の実現 細かい変更に分割できた • 漸進的な変更PRを9個+破壊的変更PRを1個に分割 • 1回あたりのレビュー負荷を大きく削減 • 問題が生じた場合も傷が浅いので修正が容易
漸進的な移行の実現 その他の工夫:古い記法を誤って使わない対策 • 例えば、findOneでwhereを省略する形をもう使ってほしくない • 使えなくなるメソッドオーバーロードを JSDocで@deprecateする ◦ deprecateされたメソッドはエディタ上で取り消し線表示になる ◦
同じメソッドでも、引数の型が特定の形の場合にのみ deprecateできる • 他の開発メンバーが誤って古い記法を使うことをうまく抑止できた
自動テストを活用した影響調査
動作を壊さずに移行する 記法は移行できた!でもちゃんと動くの? • 今まではTypeORM 0.2上で動かしているだけだった • TypeORM 0.3に切り替えると、内部コードも変更される ◦ 挙動が壊れる可能性がある
• どうやって確認すれば良いか? ◦ ORMは全機能に関わるので範囲が広すぎる・・・
動作を壊さずに移行する 自動テストに身を任せよう • 今回のコードベースには自動化された APIテストがあった ◦ DBに接続するので、TypeORMの挙動も含めた確認が可能 • テストケース回す→問題点をデバッガで追いかけて潰す の繰り返し
動作を壊さずに移行する 自動テストで大半の問題を発見できた • 型レベルでは発見できない破壊的変更への対処漏れ • TypeORM内部の日付処理の修正による破壊 (not well-documented) • TypeORM0.3で新たに混入したバグ
自動テスト最高!!
まとめ
まとめ これらの手順で移行した結果・・・ • 1人月+レビュー工数でアップデート作業を乗り切れた • 移行後も大きな問題は発生しなかった・・・はず ◦ 自動テストでほとんどの問題を洗い出せていた ◦ 移行後はあまり面倒見れていないので気づいてないだけかも
まとめ 本日のポイント • ts-morphによるコード自動操作は便利 • 自動テストはコード変更のためのお守り 面倒な作業にts-morphを是非活用してみてください!
ご清聴ありがとうございました!