進行状況の追跡を始めよう
Trailhead のホーム
Trailhead のホーム

作業単位の原則について

学習の目的

この単元を完了すると、次のことができるようになります。

  • DML 操作を効果的に管理し、部分的なデータベース更新を回避する。
  • パターンの Apex 実装の利点を理解する。
  • 作業単位パターンを、前の単元の applyDiscount サービスメソッドに適用する。

作業単位の原則

作業単位とは、トランザクション管理を実装するときのコードの繰り返しを低減し、対応付けおよびリストの拡張利用による DML 一括処理化に合わせるためのコーディングオーバーヘッドを削減する設計パターンです。サービスレイヤ実装に必須ではありませんが、便利です。使用前と後の例を示し、どのように機能するか説明します。

このモジュールで使用する作業単位パターンは、Martin Fowler が説明しているパターンに基づいています。つまり、「ビジネストランザクションに影響されるオブジェクトのリストを管理し、変更以外の記述と並行処理問題の解決を調整します」

Force.com プラットフォームでは、次の使用事例を処理するパターンに変換されます。

  • 特定のビジネス要件を実装するためのレコードの更新、挿入、削除の記録
  • 子または関連レコードを短いコードで簡単に挿入するためのレコード関係の記録
  • データベースへの書き込み (またはコミット) 要求時のすべての収集済みレコードの一括処理化
  • SavePoint で実行された DML のラップ (開発者は、記述されたすべてのサービスメソッドごとに実装しなくてすむ)

作業単位なしでのサービスメソッドの実装

作業単位パターンで実現できる内容を理解するために、まず、各サービスメソッドで、作業単位なしで記述する必要のあるコードを確認します。これまでに説明した設計のベストプラクティスには従います。特定のビジネス要件向けのコードを記述する必要があります。また、コードは、次を実装する定型コードとして機能する必要があります。

  • DML 一括処理化および最適化 - コードは、ロジックフローに応じて、商談または OpportunityLineItem レコードの一部またはすべてを更新できます。2 つのリストを作成および入力し、更新する必要のあるレコード参照のみ管理します。

  • エラー処理およびトランザクション管理 - サーバレイヤの設計の原則に従って、コードの投げる例外をコール元がキャッチするかどうかにかかわらず、すべての変更をコミットし、エラー発生時はいずれもコミットしない必要があります。例外が未処理の場合にのみ、プラットフォームは自動的にロールバックしますが、これはユーザ例外の観点では望ましいものではありません。SavePoint 機能と try/catch セマンティックを使用して、トランザクション範囲を管理することが、サービスレイヤコードのグッドプラクティスです。

次の例は、SavePoint を使用して、サービスメソッドの中でデータベース操作をカプセル化およびラップします。設計の考慮事項に従って、コール元の例外キャッチを避けるために、SavePoint を使用します (2 番目の DML ステートメントによりおそらく生じる)。また、これにより、Apex ランタイムが商談の行に更新をコミットします (1 番目の 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...
    List<OpportunityLineItem> oppLinesToUpdate = new List<OpportunityLineItem>();
    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. 1 つの作業単位を初期化して、サービスメソッドが完了するすべての作業を範囲に設定するために使用します。
  2. サービスレイヤロジックが、実行時に、作業単位を使用してレコードを登録するようにします。
  3. 作業単位を commitWork メソッドとしてコールして、DML を一括処理化して実行します。

次の図は、上記のステップを示しており、サービスメソッドコード実行に関して、各ステップの範囲を実行しています。

作業単位の含め方: 新しい作業単位インスタンスの作成、作業およびレコード変更の登録、最後に、一括処理化された作業単位のデータベースへのコミット

Note

メモ

サービス間でコールする場合、メソッドのオーバーロードを通じて、パラメータとして外部作業単位インスタンスを渡します。新しいインスタンスは作成しないでください。作業単位は、サービスメソッドのトランザクション範囲を示しているため、次に示すように、メソッドコールごとに作業単位インスタンスが 1 つのみになるようにします。

作業単位クラスを使用するには、コードがやりとりするオブジェクトのリストを使用して、クラスを作成する必要があります。登録された親および子レコードを commitWork メソッドが正しい順序で挿入するように、オブジェクトを連動関係の順序にする必要があります。fflib_SObectUnitWork 親子関係の処理は、次の単元で詳しく説明します。

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 つのコミットを実行するか、ロールバックフェーズの実行を代行するかを確認しながら、引き続き自身のデータベース更新を登録できます。

リソース