作業単位の原則について
学習の目的
この単元を完了すると、次のことができるようになります。
- DML 操作を効果的に管理し、部分的なデータベース更新を回避する。
- パターンの Apex 実装の利点を理解する。
- 作業単位パターンを、前の単元の applyDiscount サービスメソッドに適用する。
一緒にトレイルを進みましょう
エキスパートの説明を見ながらこのステップを実行したい場合は、次の動画をご覧ください。これは「Trail Together」(一緒にトレイル) シリーズの一部です。
(この動画は 54:05 の時点から始まります。戻して手順の最初から見直す場合はご注意ください。)
作業単位の原則
作業単位とは、トランザクション管理を実装するときのコードの繰り返しを低減し、対応付けおよびリストの拡張利用による DML 一括処理化に合わせるためのコーディングオーバーヘッドを削減する設計パターンです。サービスレイヤー実装に必須ではありませんが、便利です。使用前と後の例を示し、どのように機能するか説明します。
このモジュールで使用する作業単位パターンは、Martin Fowler が説明しているパターンに基づいています。つまり、「ビジネストランザクションに影響されるオブジェクトのリストを管理し、変更以外の記述と並行処理問題の解決を調整します」
Salesforce プラットフォームでは、次の使用事例を処理するパターンに変換されます。
- 特定のビジネス要件を実装するためのレコードの更新、挿入、削除の記録
- 子または関連レコードを短いコードで簡単に挿入するためのレコード関係の記録
- データベースへの書き込み (またはコミット) 要求時のすべての収集済みレコードの一括処理化
- SavePoint で実行された DML のラップ (開発者は、記述されたすべてのサービスメソッドごとに実装しなくてすむ)
作業単位なしでのサービスメソッドの実装
作業単位パターンで実現できる内容を理解するために、まず、各サービスメソッドで、作業単位を使わずに記述する必要のあるコードを確認します。これまでに説明した設計のベストプラクティスには従います。特定のビジネス要件向けのコードを記述する必要があります。また、コードは、次を実装する定型コードとして機能する必要があります。
-
DML 一括処理化および最適化 - コードは、ロジックフローに応じて、商談または OpportunityLineItem レコードの一部またはすべてを更新できます。2 つのリストを作成および入力し、更新する必要のあるレコード参照のみ管理します。
-
エラー処理およびトランザクション管理 - サーバーレイヤーの設計の原則に従って、コードの投げる例外をコール元がキャッチするかどうかにかかわらず、すべての変更をコミットし、エラー発生時はいずれもコミットしない必要があります。例外が未処理の場合にのみ、プラットフォームは自動的にロールバックしますが、これはユーザー例外の観点では望ましいものではありません。SavePoint 機能と try/catch セマンティックを使用して、トランザクション範囲を管理することが、サービスレイヤーコードのグッドプラクティスです。
次の例は、SavePoint
を使用して、設計上の考慮事項に従って、サービスメソッドの中でデータベース操作をカプセル化およびラップします。それはなぜでしょうか? 2 つ目の DML 操作が失敗するシナリオを考えてみましょう。メソッドに SavePoint
が含まれていなければ、次のようになります。
- コール元が例外を処理しない場合、Apex トランザクションのデフォルト動作に従って、最初の DML 操作を含むトランザクション全体がロールバックされます。
- コール元が例外をキャッチしても、例外の反映が許可されない、または SavePoint が復元されないと、Apex ランタイムによって商談行 (最初の DML ステートメント) に対する更新がコミットされ、データベースが部分的に更新されます。
作業単位パターンを使用しない場合は、この例に示すように、複数の DML 操作を処理することがベストプラクティスです。ただし、次のセクションで説明するように、代わりに作業単位を使って処理することもできます。
public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) { // Validate parameters // ... // Query Opportunities and Lines // ... // Update Opportunities and Lines (if present) List<Opportunity> oppsToUpdate = new List<Opportunity>(); List<OpportunityLineItem> oppLinesToUpdate = new List<OpportunityLineItem>(); // Do some work... Decimal factor = 1 - (discountPercentage==null ? 0 : discountPercentage / 100); for(Opportunity opportunity : opportunities) { // Apply to Opportunity Amount if(opportunity.OpportunityLineItems!=null && opportunity.OpportunityLineItems.size()>0) { for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems) { oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor; oppLinesToUpdate.add(oppLineItem); } } else { opportunity.Amount = opportunity.Amount * factor; oppsToUpdate.add(opportunity); } } // Update the database SavePoint sp = Database.setSavePoint(); try { update oppLinesToUpdate; update oppsToUpdate; } catch (Exception e) { // Rollback Database.rollback(sp); // Throw exception on to caller throw e; } }
作業単位パターンの Apex 実装
この単元の後半では、Martin Fowler の作業単位パターンの実装を含む Apex オープンソースライブラリを参照します。これは、fflib_SObjectUnitOfWork という 1 つのクラスで実装されます。これを別のタブで開いて、作業を進めましょう。次の単元で、このクラスを詳しく説明します。ここでは、その主要メソッドの一部のみ確認してください。
このクラスは、サービスコードが登録メソッドを通じて実行されたときに、fflib_SObjectUnitOfWork
クラスのインスタンスが、作成、更新、または削除する必要のあるレコードを収集できるようにします。また、commitWork メソッドは SavePoint および try/catch 規則をカプセル化します。
DML によるデータベースの更新は、commitWork メソッドがコールされたときにのみ発生します。このため、サービスコードは登録メソッドを必要に応じた頻度でコールでき、ループでコールすることもできます。この方法により、開発者は、複数のリストと対応付けを管理するコードではなく、ビジネスロジックに注力できます。
最後に、次の図に示すとおり、作業単位の範囲は、サービスメソッドコードの開始と終了によって決定します。サービスメソッドの範囲では、commitWork を 1 回のみコールします。
サービスコードメソッドに作業単位を含めるには、次のステップに従います。
- 1 つの作業単位を初期化して、サービスメソッドが完了するすべての作業を範囲に設定するために使用します。
- サービスレイヤーロジックが、実行時に、作業単位を使用してレコードを登録するようにします。
- 作業単位を commitWork メソッドとしてコールして、DML を一括処理化して実行します。
次の図は、上記のステップを示しており、サービスメソッドコード実行に関して、各ステップの範囲を実行しています。
作業単位クラスを使用するには、コードがやり取りするオブジェクトのリストを使用して、クラスを作成する必要があります。登録された親および子レコードを commitWork メソッドが正しい順序で挿入するように、オブジェクトを連動関係の順序にする必要があります。fflib_SObjectUnitWork 親子関係の処理は、次の単元で詳しく説明します。
fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType> { OpportunityLineItem.SObjectType, Opportunity.SObjectType } );
作業単位を使用したサービスメソッドの実装
次の例では、前の単元で作成したサービスに作業単位パターンを適用します。変更されていないコードは表示されていません。リストが消えています。また、fflib_SObjectUnitOfWork
クラスがすべて処理するため、SavePoint に try/catch 定型コードがありません。
public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) { // Unit of Work fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType> { OpportunityLineItem.SObjectType, Opportunity.SObjectType } ); // Validate parameters // ... // Query Opportunities and Lines // ... // Update Opportunities and Lines (if present) // ... for(Opportunity opportunity : opportunities) { // Apply to Opportunity Amount if(opportunity.OpportunityLineItems!=null && opportunity.OpportunityLineItems.size()>0) { for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems) { oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor; uow.registerDirty(oppLineItem); } } else { opportunity.Amount = opportunity.Amount * factor; uow.registerDirty(opportunity); } } // Commit Unit of Work uow.commitWork(); }
fflib_SObjectUnitOfWork クラスは DML 操作を集約し、commitWork メソッドがコールされたときに SavePoint にラップします。
深かったり、複数のクラスを含んだりする、より複雑なコードでは、SObjectUnitOfWork を渡すこともできます (または静的を利用します)。コールされたコードは、作業単位の所有者 (この場合はサービスレイヤー) が 1 つのコミットを実行するか、ロールバックフェーズの実行を代行するかを確認しながら、引き続き自身のデータベース更新を登録できます。
リソース
- Apex Enterprise Patterns - GitHub Repo (Apex エンタープライズパターン - GitHub リポジトリ)
- 関心の分離 (Wikipedia)
- Martin Fowler’s Enterprise Architecture Patterns (Martin Fowler のエンタープライズアーキテクチャパターン)
- Martin Fowler’s Unit of Work Patterns (Martin Fowler の作業単位パターン)
- Managing your DML and Transactions with a Unit of Work (作業単位による DML とトランザクションの管理)
- Doing more work with the Unit of Work (作業単位の活用)