Salesforce レコードロッキングをマスターする:開発者のためのデータ整合性ガイド

概要とビジネスシーン

Salesforce のレコードロッキングは、複数のトランザクションが同時に同じレコードを更新しようとした際に発生するデータの競合を防ぎ、データの整合性を保証する重要なメカニズムです。これにより、意図しないデータの上書きや不整合な状態への陥るリスクを最小限に抑えることができます。

実際のビジネスシーン

シーンA:金融業界 - 口座残高の同時更新

  • ビジネス課題:オンラインバンキングシステムで、複数のユーザーや自動プロセスが同時に同じ口座の残高を更新しようとすると、競合状態が発生し、最終的な残高が誤って計算される可能性があります。
  • ソリューション:Apex トランザクション内で FOR UPDATE 句を使用して口座レコードを明示的にロックします。これにより、更新処理中は他のトランザクションがその口座レコードを更新できなくなり、データの一貫性が保たれます。
  • 定量的効果:年間で発生するデータ不整合による顧客クレームを 95% 削減し、手動によるデータ修正にかかる時間を月間 200 時間削減。

シーンB:在庫管理 - 商品在庫の引当処理

  • ビジネス課題:Eコマースサイトで人気商品が瞬時に売れる場合、複数の顧客が同時に最後の1点を購入しようとすると、在庫がマイナスになったり、重複して販売される「売り越し」が発生する可能性があります。
  • ソリューション:注文処理時に、対象となる商品在庫レコードを FOR UPDATE でロックし、在庫チェックと引当処理を単一のトランザクション内で実行します。
  • 定量的効果:売り越しによる顧客への出荷遅延やキャンセルが 99% 減少し、顧客満足度が 15% 向上。

シーンC:カスタマーサービス - ケースの同時編集

  • ビジネス課題:カスタマーサービスセンターで、緊急性の高いケースに対して複数のエージェントが同時に作業を行う際、一方が更新した内容をもう一方が気づかずに上書きしてしまう、または誤った情報に基づいて対応を進めてしまうリスクがあります。
  • ソリューション:Salesforce の標準 UI におけるレコード編集時の自動ロック機能(または Apex での明示的な FOR UPDATE)を活用し、編集中は他のエージェントに通知したり、編集を制限します。
  • 定量的効果:ケース対応における情報上書きミスが 90% 削減され、問題解決までの平均時間が 10% 短縮。

技術原理とアーキテクチャ

Salesforce におけるレコードロッキングの主要なメカニズムは、SOQL クエリの FOR UPDATE 句を使用することです。この句が SOQL クエリに追加されると、Salesforce のデータベースは取得したレコードに対して排他ロック(X-lock)を設定します。

基礎的な動作メカニズムは以下の通りです。

  1. トランザクションAが FOR UPDATE 句を含む SOQL クエリを実行し、レコードXを取得します。
  2. Salesforce のデータベースは、レコードXに対して排他ロックを設定します。他のトランザクションはこのレコードXを FOR UPDATE で取得したり、DML 操作で更新したりすることができなくなります(UNABLE_TO_LOCK_ROW エラーが発生)。
  3. トランザクションAはレコードXに対する DML 操作(更新、削除など)を実行します。
  4. トランザクションAがコミットされるか、ロールバックされると、レコードXに設定されたロックが解放されます。

主要コンポーネントと依存関係は、Apex トランザクション、SOQL クエリ、DML 操作、そして underlying の Salesforce データベースです。これらが連携して、指定されたレコードに対する排他制御を実現します。

データフロー

ステップ 説明 主要コンポーネント
1. トランザクション開始 Apex メソッドまたはトリガーの実行により、新しいデータベース トランザクションが開始されます。 Apex Runtime, Transaction Manager
2. レコードの検索とロック FOR UPDATE 句付き SOQL クエリで対象レコードを検索し、データベースレベルで排他ロックをかけます。 SOQL Engine, Database (underlying)
3. レコードの更新 ロックされたレコードに対してビジネスロジックに基づいた DML 操作(更新など)を実行します。 DML Engine, Apex Business Logic
4. トランザクション終了 トランザクションが正常にコミットされるか、エラーによりロールバックされます。 Transaction Manager
5. ロック解放 トランザクションの終了と同時に、対象レコードのロックが解放され、他のトランザクションがアクセス可能になります。 Database (underlying)

ソリューション比較と選定

レコードロッキングの実現にはいくつかの方法がありますが、それぞれ適用シーンや特性が異なります。

ソリューション 適用シーン パフォーマンス Governor Limits 複雑度
SOQL FOR UPDATE (レコードロッキング) 複数のプロセスやユーザーによるデータ競合がクリティカルな場合(例:金融取引、在庫引当)。厳密なデータの整合性が必要。 高い整合性保証。ただし、ロック待機による処理遅延やデッドロックのリスクあり。 SOQL クエリ行数、CPU 時間、DML ステートメント数など、標準 Apex Limits が適用。ロックタイムアウトにも注意。 中:FOR UPDATE 句の追加と、エラーハンドリング(UNABLE_TO_LOCK_ROW)の実装が必要。
代替案1: Apex トランザクション (標準 DML) 一般的な DML 操作。通常はSalesforceが自動でロックを管理するが、明示的な排他制御は行わない。競合時のリトライや楽観的ロック(システムが衝突を検知し、ユーザーに再試行を促す)で対応可能な場合。 高速。競合発生時のエラーハンドリング(例:FIELD_CUSTOM_VALIDATION_EXCEPTION)を適切に実装すれば高いスループットを維持。 標準 Apex Limits が適用。 低:特別な構文は不要。
代替案2: カスタム論理ロック (Platform Cache, Custom Object) データベースロックが重すぎる、または異なるオブジェクトや外部システムとの連携で広範囲なロックが必要な場合。パフォーマンスが最優先で、多少のデータ整合性の緩和が許容される場合。 設計次第で高いスケーラビリティ。ただし、ロック管理ロジックのオーバーヘッドが発生。 Platform Cache の容量、カスタムオブジェクトのレコード数など、利用する機能に応じた Limits が適用。 高:ロック管理ロジックを自前で実装する必要があり、複雑。

record locking を使用すべき場合

  • ✅ 複数のユーザーや自動プロセスが同時に同じレコードを更新する可能性があり、その結果としてデータの不整合が発生するとビジネスに重大な影響がある場合。
  • ✅ 金融取引の残高更新、在庫の引当、ポイントの付与/消費など、厳密な排他制御とデータの原子性 (Atomic Transaction) が求められるビジネスロジック。
  • ✅ デッドロックの可能性を許容し、適切なエラーハンドリングとリトライ戦略を実装できる場合。
  • ❌ 不適用シーン:リードや取引先など、排他制御よりも並行性を優先する場合。大規模なレコードセットに対する一括更新で、デッドロックのリスクが高く、スループットが低下する可能性がある場合。

実装例

ここでは、SOQL の FOR UPDATE 句を使用してレコードをロックし、更新する具体的な Apex コード例を示します。これは、ある取引先の特定のフィールド(例: Custom_Balance__c)を安全に更新するシナリオを想定しています。

public class AccountLockerService {

    /**
     * @description 指定された取引先をロックし、カスタム残高を更新します。
     * @param accountId ロックおよび更新対象の取引先ID
     * @param amount 更新する残高の変化量
     * @return 更新後の取引先
     */
    public static Account updateAccountBalance(Id accountId, Decimal amount) {
        // 新しいデータベースのセーブポイントを作成。これにより、エラー発生時にここまでロールバックできる。
        Savepoint sp = Database.setSavepoint();

        try {
            // ① SOQL FOR UPDATE 句を使用して取引先レコードをロックして取得
            // これにより、このトランザクションがコミットまたはロールバックされるまで、
            // 他のトランザクションがこのレコードを更新できなくなります。
            List<Account> accounts = [SELECT Id, Name, Custom_Balance__c FROM Account WHERE Id = :accountId FOR UPDATE];

            // レコードが見つからない場合はエラーを投げる
            if (accounts.isEmpty()) {
                throw new CustomException('取引先ID ' + accountId + ' が見つかりません。');
            }

            Account accToUpdate = accounts[0];

            // ② ロックされたレコードのカスタム残高を更新
            if (accToUpdate.Custom_Balance__c == null) {
                accToUpdate.Custom_Balance__c = 0;
            }
            accToUpdate.Custom_Balance__c += amount;

            // ③ 更新された取引先レコードを保存
            // ロックされたレコードに対するDML操作は安全に実行されます。
            update accToUpdate;

            // 更新後のレコードを返却
            return accToUpdate;

        } catch (QueryException e) {
            // ロックの取得に失敗した場合(例: UNABLE_TO_LOCK_ROW)
            // Salesforce Developer Docs: QueryException - UNABLE_TO_LOCK_ROW
            System.debug('ERROR: レコードロック中に問題が発生しました。' + e.getMessage());
            Database.rollback(sp); // セーブポイントまでロールバック
            throw new CustomException('レコードロック中に競合が発生しました。しばらくしてから再試行してください。');
        } catch (DmlException e) {
            // DML操作中にデッドロックなどが発生した場合
            System.debug('ERROR: DML操作中に問題が発生しました。' + e.getMessage());
            Database.rollback(sp); // セーブポイントまでロールバック
            throw new CustomException('データの更新中にエラーが発生しました。' + e.getMessage());
        } catch (Exception e) {
            // その他の予期せぬエラー
            System.debug('ERROR: 予期せぬエラーが発生しました。' + e.getMessage());
            Database.rollback(sp); // セーブポイントまでロールバック
            throw e; // エラーを再スロー
        }
    }

    // カスタム例外クラス(必要に応じて定義)
    public class CustomException extends Exception {}
}

実装ロジック解析:

  1. updateAccountBalance メソッドは、指定された accountIdamount を受け取ります。
  2. Database.setSavepoint() を使用してセーブポイントを作成し、エラー発生時に処理をロールバックできるようにします。
  3. [SELECT ... FOR UPDATE] クエリを実行して、対象の取引先レコードをデータベースレベルでロックします。これにより、このトランザクションが完了するまで、他のトランザクションがこのレコードを更新できなくなります。
  4. ロックされたレコードの Custom_Balance__c フィールドを更新します。
  5. update accToUpdate; ステートメントで変更をデータベースにコミットします。このDML操作は、レコードがロックされているため安全です。
  6. try-catch ブロックを使用して、ロックの失敗(QueryException - UNABLE_TO_LOCK_ROW)やDML操作中のエラー(DmlException)を適切に処理し、必要に応じてロールバックします。これにより、デッドロックや予期せぬエラーが発生した場合でも、データの一貫性を保つことができます。

注意事項とベストプラクティス

権限要件

  • 対象オブジェクト(上記の例では Account)に対する「参照」および「編集」権限。
  • Apex クラスを実行するためのプロファイルまたは権限セットでの「Apex クラスのアクセス」設定。

Governor Limits

  • SOQL クエリの行数:FOR UPDATE 句を使用する SOQL クエリは、最大 50,000 行までしか取得できません。大量のレコードをロックする必要がある場合は、バッチ処理や適切なフィルタリングを検討してください。
  • CPU 時間:ロック待機時間も CPU 時間にカウントされる可能性があります。複雑なロジックや長いトランザクションはタイムアウトのリスクを高めます。
  • DML ステートメント数:トランザクションあたりの DML ステートメント数(150)や DML レコード数(10,000)の制限も依然として適用されます。
  • ロックタイムアウト:Salesforce はレコードロックの取得に約 10 秒のタイムアウトを設定しています。この時間内にロックが取得できない場合、UNABLE_TO_LOCK_ROW エラーが発生します。

エラー処理

  • UNABLE_TO_LOCK_ROWこれは最も一般的なレコードロック関連のエラーです。try-catch ブロックでこの例外を捕捉し、ユーザーフレンドリーなメッセージを表示したり、処理を再試行するロジックを実装したりすることが重要です。
  • デッドロック:複数のトランザクションが互いにロックを待機し合う状態です。Salesforce はデッドロックを検知すると、通常、いずれかのトランザクションをロールバックして解放します。DmlException として捕捉し、適切な処理を行う必要があります。
  • ロールバック:Database.setSavepoint()Database.rollback() を使用して、エラー発生時にトランザクション全体または一部の変更を元に戻し、データの整合性を維持します。

パフォーマンス最適化

  1. ロック範囲を最小限に:必要なレコードのみを FOR UPDATE でロックし、必要最小限の期間だけロックを保持するようにします。関連レコード全体をロックする必要がない限り、対象レコードの特定フィールドのみを更新するように設計します。
  2. トランザクションを短く保つ:ロックを保持する期間が長ければ長いほど、他のトランザクションがロックを待機する可能性が高まります。トランザクション内の処理を迅速に完了させ、ロックを速やかに解放するように設計します。
  3. ロック順序を標準化する:複数のレコードや異なるオブジェクトのレコードをロックする必要がある場合、常に同じ順序でロックを取得するようにコードを標準化します。これにより、デッドロックのリスクを大幅に軽減できます。
  4. 非同期処理の検討:即時性よりも最終的な整合性が重要な場合は、Queueable Apex や Batch Apex を使用して、競合の少ない時間帯に処理を実行することを検討します。

よくある質問 FAQ

Q1:レコードロックはいつ解放されますか?

A1:レコードロックは、FOR UPDATE 句を含むトランザクションが正常にコミットされるか、またはエラーによってロールバックされた直後に解放されます。

Q2:ロックされているレコードを更新しようとするとどうなりますか?

A2:別のトランザクションによってロックされているレコードを FOR UPDATE で取得しようとしたり、DML 操作で更新しようとすると、UNABLE_TO_LOCK_ROW エラーが発生します。これは QueryException または DmlException として捕捉できます。

Q3:デッドロックはどのように避けますか?

A3:デッドロックを避けるための最も効果的な方法は、複数のレコードをロックする場合、常に同じ順序でロックを取得するようにプログラムのロジックを設計することです。また、トランザクションを可能な限り短く保ち、不要なレコードをロックしないことも重要です。


まとめと参考資料

Salesforce のレコードロッキングは、特に複数のユーザーやプロセスが同時に同じデータを操作するようなシナリオにおいて、データの整合性を維持するための不可欠なツールです。SOQL の FOR UPDATE 句を適切に使用することで、競合状態を管理し、ビジネスデータの正確性を保証できます。

しかし、その強力な機能と引き換えに、デッドロックのリスクやパフォーマンスへの影響も考慮する必要があります。実装者は、適切なエラー処理、トランザクションの短縮、ロック順序の標準化といったベストプラクティスを遵守することで、堅牢で効率的なシステムを構築できます。

公式リソース

  • 📖 公式ドキュメント:SOQL FOR UPDATE
    https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_for_update.htm
  • 📖 公式ドキュメント:DML Statement Guidelines
    https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_dml_statements.htm#apex_dml_guidelines_for_update
  • 🎓 Trailhead モジュール:Apex Basics & Database
    https://trailhead.salesforce.com/content/learn/modules/apex_basics_database/apex_basics_database_dml

コメント