Salesforce開発者としてのキャリアで、managed packagesと密接に関わる開発をしてきた中で、今でも後悔している設計判断がある。それは、自社開発のApexコードからmanaged packageが提供するCustom ObjectやCustom Fieldを、パッケージプレフィックス付きで**直接参照してしまったこと**だ。
Managed Packageオブジェクトのハードコード参照という痛恨の極み
当時、我々が利用していたあるmanaged packageは、主要なビジネスロジックを担うCustom Object群と、それらを拡張するためのCustom Field群を提供していた。例えば、MyPackage__Order__cやMyPackage__Order__c.MyPackage__Status__cといった具合だ。
自社の要件に合わせて、これらのパッケージオブジェクトに対してトリガーを書いたり、バッチ処理でデータを集計したりする必要があった。何も考えずに、以下のようなApexコードを書いてしまったのだ。
public class MyOrderProcessor {
public void processOrders() {
// 当時はこれで良いと判断した。まさか後で地獄を見るとは。
List<MyPackage__Order__c> orders = [
SELECT Id, MyPackage__Status__c, MyPackage__OrderTotal__c, MyPackage__Customer__r.Id
FROM MyPackage__Order__c
WHERE MyPackage__Status__c = 'Pending'
];
for (MyPackage__Order__c order : orders) {
// 何らかの処理...
System.debug('Processing Order: ' + order.Id + ' Status: ' + order.MyPackage__Status__c);
// 今なら絶対にこんな直接参照はしない
}
}
}
SOQLクエリだけでなく、DML操作やApexクラス内での型定義も同様だった。
// CustomObjectの型を直接指定 MyPackage__Order__c newOrder = new MyPackage__Order__c(); newOrder.MyPackage__Status__c = 'New'; insert newOrder; // このDMLも直接オブジェクトを参照している
なぜそんなことをしてしまったのか
当時の私は、主に以下の理由でこのような設計を選択した。
- 実装スピードの優先: 最も直接的でシンプルな方法だった。IDEのオートコンプリートも効くし、開発効率は高かった。
- 知識不足: managed packageの将来的なアップグレードパスや、API名変更のリスクに対する認識が甘かった。「パッケージベンダーがそんな根本的なAPI名を変えるわけがないだろう」という根拠のない思い込みがあった。
- 動的SOQLへの抵抗: 動的SOQL (`Database.query()`) はGovernor Limitsへの影響や、セキュリティ(SOQLインジェクション)への懸念から、できるだけ避けたいと考えていた。また、コンパイル時にエラーチェックができないという不安もあった。
- Describe APIへの理解不足: `Schema.SObjectType` や `Schema.DescribeFieldResult` を使えば、オブジェクト名やフィールド名を動的に取得できることは知っていたが、そのコストや実装の複雑さに対して、直接参照のメリット(簡潔さ)が勝ると判断してしまった。
具体的に何が起きたか:アップグレードの悪夢
数年後、そのmanaged packageのメジャーバージョンアップが発表された。新機能の追加やパフォーマンス改善のためには、アップグレードが必須だった。ベンダーから送られてきたリリースノートを読んで、血の気が引いた。
新しいバージョンでは、一部の重要なCustom ObjectのAPI名や、主要なCustom FieldのAPI名が変更されていたのだ。例えば、MyPackage__Order__cがMyPackage__Transaction__cに、MyPackage__Status__cがMyPackage__LifecycleStatus__cになるといった具合だ。
旧バージョンからのアップグレードパスは提供されていたが、パッケージオブジェクトを直接参照している我々の自社コードは、文字通り壊滅的な影響を受けた。
- 全てのSOQLクエリでSyntax Errorが発生。
- Apexクラスのコンパイルが通らない。
- `insert`や`update`などのDML操作も無効に。
- 型定義自体が変わったため、テストクラスも全滅。
結局、我々はmanaged packageのアップグレードと並行して、自社開発のApexコードをほぼ全て見直す羽目になった。変更箇所は数百ファイル、数万行に及び、パッケージのアップグレード自体よりも、自社コードの修正とテストに膨大な時間とリリソースを費やした。この経験は、本当に苦い記憶として残っている。
当時の判断を振り返る
あの時、もう少し時間をかけてパッケージのアーキテクチャや今後のロードマップについて深く検討していれば、こんな事態は避けられたはずだ。特に、ISVが提供するmanaged packageは、その性質上、内部的なオブジェクト構造やAPI名が変更されるリスクがあることをもっと重く見るべきだった。
「一度入れたパッケージは簡単には入れ替えられない」という意識はあったが、「パッケージの内部構造が変わることで自社コードが壊れる」という発想が抜け落ちていた。これは完全に私のアーキテクチャ設計における見識不足だった。
今ならどうするか(苦い教訓として)
今、同様の状況に直面したら、絶対にmanaged packageのオブジェクトやフィールドを直接参照するようなコードは書かない。
選択肢としては、以下を検討するだろう。
- Custom Metadata Type/Custom Settingsでの設定化: オブジェクト名やフィールド名をCustom Metadata Type (またはCustom Settings) に設定値として持たせ、Apexコードからは設定値を取得して動的にSOQLを構築する。これにより、パッケージ側の変更があっても、コード修正ではなく設定変更で対応できる可能性が高まる。
// Custom Metadata Typeの例: MyPackageObjectMapping__mdt (API Name, FieldNameなどを保持) public class MyOrderProcessorV2 { public void processOrders() { String objectApiName = 'MyPackage__Order__c'; // 実際の値はCustom Metadata Typeから取得 String statusField = 'MyPackage__Status__c'; // 同上 String orderTotalField = 'MyPackage__OrderTotal__c'; // 同上 // 動的SOQLで参照 String query = 'SELECT Id, ' + statusField + ', ' + orderTotalField + ' FROM ' + objectApiName + ' WHERE ' + statusField + ' = \'Pending\''; List<SObject> orders = Database.query(query); for (SObject order : orders) { System.debug('Processing Order: ' + order.Id + ' Status: ' + order.get(statusField)); } } }ただし、動的SOQLはGovernor Limits(特にHeap Size)やパフォーマンス、セキュリティ(SOQLインジェクションのリスクは適切にエスケープすれば回避可能)への配慮が不可欠だ。当時はこの点を過度に恐れていた節がある。また、Describe APIを多用しすぎると、メソッドコール数の制限にも注意が必要だ。⚠️ 公式ドキュメントでDescribe APIのコール制限を確認する必要がある。
- Apex Type Casting: 多少強引だが、`SObject`として取得した後に`Type.forName()`などでパッケージオブジェクトの型を動的に取得し、キャストする方法も無くはない。しかし、これはこれで保守性が落ちる可能性もある。
- ラッパーオブジェクトの導入: パッケージオブジェクトのAPI名を直接扱わず、自社で定義したラッパーオブジェクトを経由してアクセスする。ラッパー内でパッケージのAPI名を隠蔽し、変更があった際にはラッパー内部のみを修正する。これは手間がかかるが、より堅牢な設計になる。
どの方法も一長一短あるが、少なくとも当時のように「そのまま参照」という安易な選択はしない。
これは当時の自分向けのメモだ。あの時の判断ミスは、結果的にとてつもない負債となり、多くの開発工数を無駄にした。managed packageを使う際は、その「管理されている」という性質の裏にある、ベンダー依存のリスクと、それに対する自社コードの耐障害性を真剣に考えるべきだった。
コメント
コメントを投稿