Apex での作業単位の原則の適用
学習の目的
この単元を完了すると、次のことができるようになります。
- 作業単位クラスとそのメソッドを記述する。
- Apex で fflib_SObjectUnitOfWork クラスとその API を利用する。
一緒にトレイルを進みましょう
エキスパートの説明を見ながらこのステップを実行したい場合は、次の動画をご覧ください。これは「Trail Together」(一緒にトレイル) シリーズの一部です。
(この動画は 1:08:31 の時点から始まります。戻して手順の最初から見直す場合はご注意ください。)
作業単位による効率化
これは高度なコンテンツであるため、複雑なシナリオで確認しましょう。商談とすべての必須の連動レコードをゼロから作成するという量の多い作業を行います。次のテスト設定コードは、これを作業単位なしで完了する方法を示しています。長いと思いませんか?
List<Opportunity> opps = new List<Opportunity>(); List<List<Product2>> productsByOpp = new List<List<Product2>>(); List<List<PricebookEntry>> pricebookEntriesByOpp = new List<List<PricebookEntry>>(); List<List<OpportunityLineItem>> oppLinesByOpp = new List<List<OpportunityLineItem>>(); for(Integer o=0; o<10; o++) { Opportunity opp = new Opportunity(); opp.Name = 'Opportunity ' + o; opp.StageName = 'Open'; opp.CloseDate = System.today(); opps.add(opp); List<Product2> products = new List<Product2>(); List<PricebookEntry> pricebookEntries = new List<PricebookEntry>(); List<OpportunityLineItem> oppLineItems = new List<OpportunityLineItem>(); for(Integer i=0; i<o+1; i++) { Product2 product = new Product2(); product.Name = opp.Name + ' : Product : ' + i; products.add(product); PricebookEntry pbe = new PricebookEntry(); pbe.UnitPrice = 10; pbe.IsActive = true; pbe.UseStandardPrice = false; pbe.Pricebook2Id = Test.getStandardPricebookId(); pricebookEntries.add(pbe); OpportunityLineItem oppLineItem = new OpportunityLineItem(); oppLineItem.Quantity = 1; oppLineItem.TotalPrice = 10; oppLineItems.add(oppLineItem); } productsByOpp.add(products); pricebookEntriesByOpp.add(pricebookEntries); oppLinesByOpp.add(oppLineItems); } // Insert Opportunities insert opps; // Insert Products List<Product2> allProducts = new List<Product2>(); for(List<Product2> products : productsByOpp) { allProducts.addAll(products); } insert allProducts; // Insert Pricebooks Integer oppIdx = 0; List<PricebookEntry> allPricebookEntries = new List<PricebookEntry>(); for(List<PriceBookEntry> pricebookEntries : pricebookEntriesByOpp) { List<Product2> products = productsByOpp[oppIdx++]; Integer lineIdx = 0; for(PricebookEntry pricebookEntry : pricebookEntries) { pricebookEntry.Product2Id = products[lineIdx++].Id; } allPricebookEntries.addAll(pricebookEntries); } insert allPricebookEntries; // Insert Opportunity Lines oppIdx = 0; List<OpportunityLineItem> allOppLineItems = new List<OpportunityLineItem>(); for(List<OpportunityLineItem> oppLines : oppLinesByOpp) { List<PricebookEntry> pricebookEntries = pricebookEntriesByOpp[oppIdx]; Integer lineIdx = 0; for(OpportunityLineItem oppLine : oppLines) { oppLine.OpportunityId = opps[oppIdx].Id; oppLine.PricebookEntryId = pricebookEntries[lineIdx++].Id; } allOppLineItems.addAll(oppLines); oppIdx++; } insert allOppLineItems;
では、作業単位パターンを使用して、上記のコードを書き直しましょう。まず、作業単位インスタンスの作成から開始します。コンストラクターに渡されるオブジェクトを連動関係の順序にして、commitWork メソッドが正しい順序でそれらを挿入できるようにする必要があります。
// Create a Unit Of Work fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new Schema.SObjectType[] { Product2.SObjectType, PricebookEntry.SObjectType, Opportunity.SObjectType, OpportunityLineItem.SObjectType } );
前の単元では、applyDiscount サービスメソッドは、registerDirty メソッドを使用して、更新する商談および商談品目レコードを登録しました。registerNew メソッドは、新しいレコードを挿入します。また、子レコードが挿入される場合、このメソッドが、正しい親 ID が適用されていることを挿入前に確認します。これは、22 行目のアクションで確認できます。その他のリレーションを作成する場合、registerRelationship メソッドをコールすることもできます (21 行目)。では、作業単位に作業させましょう! と、ダジャレはこれくらいにしておきますね。
// Do some work! for(Integer o=0; o<10; o++) { Opportunity opp = new Opportunity(); opp.Name = 'UoW Test Name ' + o; opp.StageName = 'Open'; opp.CloseDate = System.today(); uow.registerNew(opp); for(Integer i=0; i<o+1; i++) { Product2 product = new Product2(); product.Name = opp.Name + ' : Product : ' + i; uow.registerNew(product); PricebookEntry pbe = new PricebookEntry(); pbe.UnitPrice = 10; pbe.IsActive = true; pbe.UseStandardPrice = false; pbe.Pricebook2Id = Test.getStandardPricebookId(); uow.registerNew(pbe, PricebookEntry.Product2Id, product); OpportunityLineItem oppLineItem = new OpportunityLineItem(); oppLineItem.Quantity = 1; oppLineItem.TotalPrice = 10; uow.registerRelationship(oppLineItem, OpportunityLineItem.PricebookEntryId, pbe); uow.registerNew(oppLineItem, OpportunityLineItem.OpportunityId, opp); } }
前の単元で説明したとおり、これらのメソッドは、データベース操作を実行しません。操作を実行するには、commitWork メソッドで作業をコミットする必要があります。これは、適切な子から親の順序で登録されたレコードで正しい DML ステートメントをコールします。また、リレーションについては、親 ID を子レコードに割り当てて、データベースに挿入するようにオブジェクトモデルを効果的にまとめます。
// Commit the work to the database! uow.commitWork();
コードが短いだけでなく、ややこしいリストが含まれておらず、ロジック面の内容を確認しやすくなっています。使用できるその他の登録メソッドの例は、次のとおりです。
// Inserts new Opportunity when committing uow.registerNew(opp); // Inserts new Opportunity Line Item and associates it with the given Opportunity record when committing uow.registerNew(oppLineItem, OpportunityLineItem.OpportunityId, opp); // Relates the given Opportunity Line Item to the given Price Book Entry when committing uow.registerRelationship(oppLineItem, OpportunityLineItem.PricebookEntryId, pbe);
まとめ
このモジュールでは、独自の考慮事項と関心を持つレイヤーで、アプリケーション実装を考える利点について学習しました。生き物のように、それぞれが、アプリケーションを堅牢にして、長期間にわたり持続するようにするための役割を担っています。
アプリケーションの心臓であるビジネスロジックを分離することで、この最初の一歩を踏み出しました。ここで終了することも、引き続きアプリケーションに必要なオブジェクト動作 (トリガーコード) や情報のクエリに対して関心の分離を適用することもできます。「Apex エンタープライズパターン: ドメインおよびセレクターレイヤー」モジュールに続いて、ドメインおよびセレクターアプリケーションレイヤーについてもご確認ください。そして、サービスの構築を楽しんでください!
課題に向けた準備
これらの課題を完了するために、いくつかのオープンソースライブラリをリリースする必要があります。fflib_SObjectUnitOfWork
クラスは、ApexMocks Framework オープンソースライブラリに従属する Apex Common オープンソースライブラリに含まれています。まず ApexMocks、次に Apex Common をインストールする必要があります。これらのライブラリとそれぞれのオープンソース使用許諾契約についての詳細は、各リポジトリを参照してください。
ライブラリを組織にインストールするには、次のリリースボタンを使用します。
ApexMocks オープンソースライブラリのリリース。
Apex Common オープンソースライブラリのリリース。
リソース
- 関心の分離 (Wikipedia)
- Martin Fowler’s Enterprise Architecture Patterns (Martin Fowler のエンタープライズアーキテクチャパターン)
- Martin Fowler’s Unit of Work Patterns (Martin Fowler の作業単位パターン)
- Apex Enterprise Patterns - GitHub Repo (Apex エンタープライズパターン - GitHub リポジトリ)
- Managing your DML and Transactions with a Unit of Work (作業単位による DML とトランザクションの管理)
- Doing more work with the Unit of Work (作業単位の活用)