通常、レコードを取得したあと元のレコードに対して更新を行うまでの間に、元のレコードが更新されないことは保証されません。ApexではクエリにFOR UPDATEキーワードを使用することで元のレコードが更新されないようロックすることが可能です。本記事では、なぜロックが必要なのか、ロックの方法と考慮事項について解説します。
排他制御(レコードロック)の重要性
レコードを参照して更新するまでの間に、他の更新がそのレコードに対して行われると不都合があるケースがあります。
例えば、承認申請された順に連番の番号をレコードに登録したい場合を考えます。
とあるレコードに連番の最大値を持たせて、参照するごとに値のカウントアップを行う方法とします。
このとき同じ処理が同時に実行されると、同じ値で上書きしてしまい、意図した動作とならない可能性があります。
この問題を回避するためにレコードを参照した時点で、他の更新が行われないようロックすることができます。
排他制御(レコードロック)の方法 ~FOR UPDATE~
レコードロックを行う方法は、クエリに「FOR UPDATE」キーワードを使用します。
クエリ対象のレコードがロックされます。
FOR UPDATEを使用する SOQL クエリでは、ORDER BY キーワードを使用できません。
Account[] accts = [SELECT Id FROM Account WHERE Name = '架空株式会社' FOR UPDATE];
ロックはトランザクションが完了すると解除されます。
Salesforce公式HELP:ロックステートメント
排他制御(レコードロック)で考慮すること
ロック中に別の更新が実行された場合
ロック中に他のクライアントが同じレコードを更新するには、トランザクションが完了してレコードのロックが解除されるまで待機する必要があります。
プロセスは新しいロックを取得する前に、ロックが解除されるのを最大 10 秒間待機します。待機時間が 10 秒を超えると、QueryException が発生します。同様に、別のクライアントが現在ロックしているレコードを更新しようとし、ロックが最大 10 秒以内に解除されない場合は、DmlException が発生します。
レコードロック中でも参照は可能
FOR UPDATEキーワードを使用したレコードのロック中でも、レコードを参照することは可能です。
クライアントがロックされているレコードを変更しようとした場合、ロックがすぐに解除されれば更新は成功する可能性があります。
つまり、2番目のクライアントが古いデータを取得して更新すると、ロックしていたクライアントの変更が上書きされる可能性があります。
これを防ぐために、2番目のクライアントも最初にレコードをロックし、最新のデータを取得してから更新を行う必要があります。(2番目のクライアントもFOR UPDATEを使用したクエリが必要)
コールアウトの実行でロックが解除される
レコードのロックは、コールアウトの実行時に自動的に解除されます。FOR UPDATE クエリが実行された可能性がある場合に、コールアウトを実行するときは注意してください。
デッドロックに注意する
Apexでもデッドロックは発生する可能性があります。
デッドロックを回避するため、Apex ランタイムエンジンでは、次の処理が行われます。
- sObject の親レコードをロックしてから子レコードをロックします。
- 同じ型の複数のレコードを編集している場合は、ID 順に sObject レコードをロックします。
デッドロックが引き起こされないように行をロックする場合、慎重に行ってください。アプリケーション内のあらゆる場所から同じ順序でテーブルと行にアクセスして、標準のデッドロック回避手法が使用されていることを確認してください。
Salesforce公式HELP:デッドロックの回避
まとめ
レコードをロックすることで予期しない更新によるトラブルを回避できます。
一方で、複雑なリレーションを持つレコードや頻繁にアクセスがあるレコードなどの場合、ロックが繰り返し発生してデッドロックやロックの待機時間超過によるエラーなどのトラブルの原因になる可能性もあります。
レコードのロックは慎重に行っていきましょう。