Managed PackageオブジェクトをApexで直接参照して後悔した話

C. 後から「やらなければよかった」と思った設計

Salesforce開発者としてのキャリアで、managed packagesと密接に関わる開発をしてきた中で、今でも後悔している設計判断がある。それは、自社開発のApexコードからmanaged packageが提供するCustom ObjectやCustom Fieldを、パッケージプレフィックス付きで**直接参照してしまったこと**だ。

Managed Packageオブジェクトのハードコード参照という痛恨の極み

当時、我々が利用していたあるmanaged packageは、主要なビジネスロジックを担うCustom Object群と、それらを拡張するためのCustom Field群を提供していた。例えば、MyPackage__Order__cMyPackage__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__cMyPackage__Transaction__cに、MyPackage__Status__cMyPackage__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を使う際は、その「管理されている」という性質の裏にある、ベンダー依存のリスクと、それに対する自社コードの耐障害性を真剣に考えるべきだった。

コメント