Apexによる高度なSalesforce取引先共有:開発者向けガイド

背景と応用シナリオ

Salesforceのデータセキュリティモデルは、その堅牢性と柔軟性で高く評価されています。組織の共有設定 (Organization-Wide Defaults, OWD)、ロール階層 (Role Hierarchy)、共有ルール (Sharing Rules) などを組み合わせることで、ほとんどのビジネス要件に対応できます。しかし、企業の成長と共に、データアクセス要件はより複雑かつ動的になることがあります。

例えば、以下のようなシナリオを考えてみましょう。

  • プロジェクトベースのアクセス:特定の取引先に対して、部門横断的なプロジェクトチームが期間限定でアクセスする必要がある。チームメンバーはロール階層上で全く異なる場所に位置している。
  • 動的な条件に基づくアクセス:取引先に関連するカスタムオブジェクト「プロジェクト」の状況が「進行中」であり、かつ担当者の地域が「東京」である場合にのみ、特定の公開グループ (Public Group) に取引先への編集アクセスを付与したい。
  • 外部システムとの連携:外部のプロジェクト管理ツールから連携された情報に基づき、リアルタイムで取引先へのアクセス権を付与または剥奪したい。

これらのシナリオでは、標準の共有ルールだけでは対応が困難です。共有ルールの条件は静的であり、関連オブジェクトの特定のレコード状態や、複数の複雑な条件の組み合わせをリアルタイムで反映させることが難しいためです。Salesforce 開発者として、このような複雑な要件を実現するために私たちが活用するのが、Apex によるプログラムでの共有 (Apex-managed Sharing) です。

この記事では、Salesforce 開発者の視点から、Apex を使用して取引先 (Account) オブジェクトの共有をプログラムで制御する方法について、その原理から具体的な実装、注意点までを詳細に解説します。


原理説明

Apex-managed Sharing の中核をなすのは、共有オブジェクト (Share Object) です。Salesforce の主要な標準オブジェクトや全てのカスタムオブジェクトには、それに対応する共有オブジェクトが自動的に作成されます。例えば、取引先オブジェクトには `AccountShare` という共有オブジェクトが存在します。

この `AccountShare` オブジェクトにレコードを作成することで、特定のユーザーやグループに対して、特定の取引先レコードへのアクセス権を付与することができます。`AccountShare` オブジェクトの主要な項目は以下の通りです。

`AccountShare` の主要項目

  • `AccountId`:共有対象となる取引先レコードの ID。
  • `UserOrGroupId`:アクセス権を付与するユーザー (User) または公開グループ (Public Group) の ID。
  • `AccessLevel`:付与するアクセスレベル。取引先の場合は `Read` (参照のみ) または `Edit` (参照・更新) を指定します。
  • `RowCause`:なぜこの共有レコードが存在するのか、その理由を示す項目です。標準の共有ルールであれば `Rule`、手動共有であれば `Manual` などが設定されます。Apex-managed Sharing を行う場合、`ApexSharingReason` (Apex 共有の理由) を使用することが強く推奨されます。

ApexSharingReason の重要性

Apex を使って共有レコードを作成する際には、`RowCause` にカスタムの `ApexSharingReason` を設定することがベストプラクティスです。これにより、なぜこの共有がプログラムによって作成されたのかを明確に追跡・管理できます。

`ApexSharingReason` は、[設定] > [セキュリティ] > [共有設定] > [取引先] のページで「Apex 共有の理由」関連リストから新規に作成します。例えば、「プロジェクトチーム共有」(`ProjectTeamSharing__c`) のような名前で作成することで、コードの可読性とメンテナンス性が大幅に向上します。

Apex コード内からは `Schema` クラスを通じてこのカスタム理由を安全に参照できます。例: `Schema.AccountShare.RowCause.ProjectTeamSharing__c`

Apex-managed Sharing の基本的な流れは以下のようになります。

  1. 共有が必要となるトリガー(レコード作成、更新など)を特定する。
  2. トリガー内で、共有対象の取引先レコードと、アクセスを付与するユーザー/グループを特定する。
  3. `AccountShare` オブジェクトのインスタンスを生成する。
  4. `AccountId`, `UserOrGroupId`, `AccessLevel`, `RowCause` を設定する。
  5. 作成した `AccountShare` レコードのリストを DML (Data Manipulation Language) の `insert` 操作でデータベースに挿入する。

この仕組みを理解することで、静的な共有ルールでは実現不可能な、動的で精緻なアクセス制御ロジックを Apex で実装することが可能になります。


示例代码

以下に、特定の取引先を、指定された公開グループに編集権限で共有する Apex クラスのサンプルコードを示します。このコードは、取引先のカスタム項目 `Project_Status__c` が `Active` になった際に起動されることを想定しています。`RowCause` には、事前に設定で作成したカスタムの `ApexSharingReason` である `Project_Team_Access__c` を使用します。

このコードは developer.salesforce.com の Apex 開発者ガイドで紹介されているプログラムによる共有の標準的なパターンに基づいています。

public class AccountSharingManager {

    // 共有を適用するメインメソッド
    public static void shareActiveProjectAccounts(List<Account> accountsToShare) {
        
        // 共有先の公開グループを取得する
        // グループ名が 'Project Team Members' であることを想定
        Group projectTeamGroup = [SELECT Id FROM Group WHERE Type = 'Regular' AND Name = 'Project Team Members' LIMIT 1];
        
        // 共有先のグループが存在しない場合は処理を中断
        if (projectTeamGroup == null) {
            System.debug('共有先の公開グループが見つかりません。');
            return;
        }

        // 作成する AccountShare レコードを格納するリストを初期化
        List<AccountShare> sharesToInsert = new List<AccountShare>();

        // Apex 共有の理由 (ApexSharingReason) をスキーマから取得
        // 'Project_Team_Access__c' は事前に設定画面で作成しておく必要があります
        String sharingReason = Schema.AccountShare.RowCause.Project_Team_Access__c;

        // 渡された取引先のリストをループ処理
        for (Account acc : accountsToShare) {
            // AccountShare オブジェクトの新しいインスタンスを作成
            AccountShare accShare = new AccountShare();

            // 共有対象の取引先 ID を設定
            accShare.AccountId = acc.Id;
            
            // アクセスを付与するグループの ID を設定
            accShare.UserOrGroupId = projectTeamGroup.Id;
            
            // アクセスレベルを 'Edit' (参照・更新) に設定
            accShare.AccessLevel = 'Edit';
            
            // なぜこの共有が行われたかの理由を設定
            accShare.RowCause = sharingReason;
            
            // 作成した共有レコードをリストに追加
            sharesToInsert.add(accShare);
        }

        // DML 操作はループの外で行う (ガバナ制限回避のベストプラクティス)
        if (!sharesToInsert.isEmpty()) {
            // Database.insert を使用して、部分的な成功を許容し、エラーハンドリングを行う
            Database.SaveResult[] saveResults = Database.insert(sharesToInsert, false);

            // DML の結果をチェックし、エラーがあればログに出力
            for (Database.SaveResult sr : saveResults) {
                if (!sr.isSuccess()) {
                    for (Database.Error err : sr.getErrors()) {
                        // エラーメッセージをデバッグログに出力
                        System.debug('共有レコードの作成に失敗しました。');
                        System.debug('対象の取引先 ID: ' + sharesToInsert.get(saveResults.indexOf(sr)).AccountId);
                        System.debug('エラー: ' + err.getStatusCode() + ': ' + err.getMessage());
                    }
                }
            }
        }
    }
}

このクラスを Account のトリガーから呼び出すことで、特定の条件を満たした際に自動的に共有ルールを適用できます。

トリガーの例

trigger AccountTrigger on Account (after update) {
    List<Account> activeProjectAccounts = new List<Account>();
    for (Account acc : Trigger.new) {
        // 以前のレコードの状態を取得
        Account oldAcc = Trigger.oldMap.get(acc.Id);

        // プロジェクトステータスが 'Active' に変更された場合のみを対象とする
        if (acc.Project_Status__c == 'Active' && oldAcc.Project_Status__c != 'Active') {
            activeProjectAccounts.add(acc);
        }
    }

    if (!activeProjectAccounts.isEmpty()) {
        AccountSharingManager.shareActiveProjectAccounts(activeProjectAccounts);
    }
}

注意事項

Apex-managed Sharing を実装する際には、いくつかの重要な点に注意する必要があります。

権限と実行コンテキスト

Apex クラスはデフォルトでシステムコンテキスト(`without sharing`)で実行されるため、実行ユーザーの権限に関係なくコードは動作します。しかし、共有レコードを挿入するためには、コードを実行するコンテキストがある程度の権限を持っている必要があります。通常、この種のロジックはトリガーなどから起動されるため問題になりにくいですが、`with sharing` キーワードが指定されたクラスから呼び出す場合は意図しない動作になる可能性があるため、実行コンテキストを常に意識してください。

組織の共有設定 (OWD)

Apex-managed Sharing は、OWD よりも厳しいアクセス権を付与することはできません。OWD はベースラインとなる最も制限の厳しいアクセスレベルを定義します。例えば、取引先の OWD が「公開/参照・更新可能」(`Public Read/Write`) である場合、全てのユーザーが既に編集権限を持っているため、Apex で共有レコードを作成する意味はありません。Apex-managed Sharing が有効なのは、OWD が「非公開」(`Private`) や「公開/参照のみ」(`Public Read Only`) の場合です。

API 制限と一括処理 (Bulkification)

共有レコードの挿入も DML 操作であるため、ガバナ制限の対象となります。サンプルコードのように、DML 操作は必ずループの外で行うようにしてください。一度に大量のレコードを処理する場合は、SOQL クエリや DML ステートメントの数に注意が必要です。

エラー処理

`Database.insert(records, allOrNone)` の第二引数を `false` に設定することで、一部のレコードでエラーが発生しても、他の成功したレコードの挿入はロールバックされません。例えば、対象のユーザーが既に同じかそれ以上のアクセス権を持っている場合、共有レコードの挿入は `MIXED_DML_OPERATION` などのエラーになります。`Database.SaveResult` を用いてこれらのエラーを適切にハンドリングし、ログに記録することが重要です。

共有の削除

アクセス権を付与するロジックだけでなく、条件が満たされなくなった場合にそのアクセス権を剥奪するロジックも同様に重要です。例えば、プロジェクトのステータスが `Completed` になった場合に、作成した `AccountShare` レコードをクエリで特定し、`delete` する処理を実装する必要があります。この際、`RowCause` に設定したカスタムの `ApexSharingReason` が、削除対象の共有レコードを特定する上で非常に役立ちます。


まとめとベストプラクティス

Apex-managed Sharing は、Salesforce 開発者が標準機能の枠を超えて、複雑で動的なデータアクセス要件に対応するための強力なツールです。

以下に、開発者として心掛けるべきベストプラクティスをまとめます。

  1. 可能な限り宣言的アプローチを優先する:Apex での実装は柔軟性が高い反面、コードのメンテナンスコストが発生します。共有ルールやフローなど、標準機能で要件を満たせる場合はそちらを優先してください。Apex-managed Sharing は、それが唯一の解決策である場合にのみ採用します。
  2. `ApexSharingReason` を必ず使用する:プログラムによって作成された共有レコードを明確に識別し、管理・デバッグを容易にするために、カスタムの `ApexSharingReason` を必ず作成し、コード内で使用してください。
  3. コードの一括処理 (Bulkify) を徹底する:トリガーから起動されるコードは、一度に最大 200 レコードを処理する可能性があります。SOQL や DML がループ内に記述されていないか、常に確認してください。
  4. 付与と剥奪のロジックをセットで考える:共有レコードを作成するロジックを実装する際は、必ずそれを削除するロジックも設計・実装してください。これにより、不要なアクセス権がシステムに残り続けることを防ぎます。
  5. 大規模データにおけるパフォーマンスを考慮する:数百万件の取引先に対して共有の再計算を行うと、パフォーマンスに影響を与える可能性があります。特に所有者の変更やロール階層の変更は大規模な再計算をトリガーします。Apex での共有ロジックも、この再計算の負荷を考慮して設計する必要があります。

Salesforce 開発者として、私たちは単にコードを書くだけでなく、プラットフォーム全体のアーキテクチャとセキュリティモデルを深く理解することが求められます。Apex-managed Sharing を適切に活用することで、セキュアでスケーラブル、かつビジネス要件に柔軟に対応できるアプリケーションを構築することが可能です。

コメント