並行モデル

アプリケーションの状態を、異なる時点または仮想的な状態として表現することを可能にする。

2005年12月12日

これは、私が2000年代半ばに行っていたエンタープライズアプリケーションアーキテクチャのさらなる開発の執筆の一部です。残念ながら、他の多くのことに気を取られてしまい、それ以上取り組む時間がありませんでした。また、近い将来も時間を見つけることができそうにありません。そのため、この資料は非常に下書きの形であり、再び取り組む時間を見つけられるまで修正や更新は行いません。

エンタープライズアプリケーションは、世界の状況に関するデータを取得する情報システムです。多くの場合、情報システムを使用する際、私たちはそのシステムがその世界に対する現在の最良の理解を反映することを望みます。また、過去の世界について知りたい、あるいは将来何が起こるかの結果を探求したい場合もあります。

ほとんどの場合、これらの代替状態を扱うために、現在の世界の状態に、これらの代替状態に関するデータを取得する機能を追加します。顧客の過去の住所の時系列プロパティは、その良い例です。

しかし、時には、過去または想像された状態における世界のすべてのニュアンスを本当に捉える、より包括的なものを求めています。並行モデルは、情報ストア全体を簡単に取得し、過去の状態、または過去、現在、未来の代替状態として表現できるようにすることで、これを行います。

仕組み

過去の状態(または仮想的な過去、現在、未来の状態)をクエリおよび操作できる情報システムについて話すとき、それは明らかにSFのように聞こえます。しかし、半ば真剣なソフトウェア開発プロジェクトは、バージョン管理システムを使用するとき、これを常に行っています。過去のコードの状態を再現したり、(ブランチを介して)代替現実を作成したり、複数のアクティブなコードラインなどの複数の代替現実を同時に保持したりできます。

バージョン管理システムがこのメガ分裂思考を処理できる能力の中心にあるのは、イベントソーシングを使用しているという事実です。コードのすべての変更がキャプチャされ、保存されます。金曜日に前の水曜日のコードの状態を要求した場合、バージョン管理システムは概念的に何もない状態から開始し、水曜日までに発生したすべてのコード変更イベントを適用して結果を提供します。「概念的に」と言うのは、おそらく正確にはそうではないからですが、観測可能な同等のことを行っています。

これを考える方法は、アプリケーションの現在の状態は、空白の初期状態を取得してイベントを再生した結果であるということです。イベントのすべてのシーケンスは異なる状態につながります。水曜日の朝の状態は、水曜日より前に発生したすべてのイベントを適用した結果であり、現在の状態は、これまでに発生したすべてのイベントを再生した結果です。(あなたがこれを水曜日の朝に読んでいるなら、最後の文が意味をなすように、数時間本を置く必要があります。)

状態をイベントの適用結果として考えることで、仮想現実を処理するアプローチが確立されます。もしデレクが先週の水曜日にあの間抜けなチェックインをしていなかったらどうなっていたかを知りたい場合、そのチェックイン以外のすべてのイベントを再生することで(ある程度)これを行うことができます。これにより、代替現実(またはおそらく現実)が得られます。これらについて考えると、銀河ヒッチハイクガイドに危険なほど近づいてしまいます。

私たちは並行モデルをコードに適用することを非常に普通に考えますが、コードを使って書くものにも適用できます。サプライチェーンを管理するアプリケーションでイベントソーシングを使用する場合、購入イベントの束を作成し、システムの安全なコピーに導入して何が起こるかを確認することで、ノンスティック糊に対する狂ったクリスマスのラッシュの影響を探求します。また、先週の並行モデルを作成することで、先週のサイレントレコードの出荷場所を確認することもできます。

並行モデルを構築するために最初に確認することは、イベントソーシングを使用するシステムがあるかどうかです。システムがイベントソーシングを念頭に置いて設計されている場合は、それが最適です。それができない場合は、ログからイベントを再構築する方法があるかもしれません。

次に必要なのは、通常の世界とは分離されたモデルでイベントを処理する方法です。これを行うには、大きく分けて2つの方法があります。1つは、独自のデータベースと環境を備えたアプリケーションの別のコピーである並行システムを構築することです。もう1つは、1つのシステムが組み込み並行モデルを切り替えることができるようにすることです。

並行システム

並行システムの最大の利点は、並行モデルを構築するためにアプリケーション自体に何もする必要がないことです。アプリケーションのコピーを作成し、永続データストアなどのリソースの使用を、コピーを使用するように切り替えます。次に、必要なイベントをフィードして実行します。

並行システムの課題は、結果をどうするかということです。アプリケーションは並列処理を認識していないため、複数の並行モデルからの結果を操作または表示できません。並行モデル処理の結果をより広範なシステムに結び付けるには、統合手法を使用する必要があります。並行システムを完全に独立したアプリケーションとして効果的に扱うことになります。

並行システムを使用する利点の1つは、並行システムのソースコードを変更する機能も備えていることです。アプリケーションに組み込まれている並行モデルでは、動作の変更は、並列化されているモデルに組み込まれている動作に限定されます。アダプティブオブジェクトモデルを使用している場合、これは重要になる可能性がありますが、限界があります。並行システムを使用すると、アプリケーションのソースコードに好きな変更を加え、これがイベントストリームに与える影響を確認できます。

組み込み並行モデル

複数の並行モデルをアプリケーションに埋め込みたい場合は、永続ストレージを複数の実際のデータソース間で簡単に切り替えられるようにする必要があります。これを行うための良い方法は、リポジトリを使用し、アプリケーションが実行時にリポジトリを切り替えることができるメカニズムを提供することです。一時的なリポジトリは永続的である必要はありません。並行モデルは一時的な傾向があるため、多くの場合、メモリ内で組み立てることができます。イベントを処理し、並行モデルから必要な情報を取得したら、リポジトリを破棄できます。

一時的なリポジトリを使用するようにシステムを切り替えるということは、最初にこのリポジトリを作成し、(通常はスキーマと不変の参照データを使用して)初期化してから、通常アプリケーションコードが認識するすべての場所でライブリポジトリを一時的なリポジトリに置き換える必要があることを意味します。これが、リポジトリの単一の参照ポイントを使用することが非常に重要である理由です。

このようにリポジトリを使用する利点の1つは、すべてをメモリ内で処理するため、はるかに高速になる可能性があることです。

どのような並列処理を行う場合でも、並列化されているモデルは代替現実の存在を認識していないという影響が常にありますが、並列世界を認識している外部システムがあります。組み込みモデルの場合、アプリケーション自体が並列対応セグメントと非対応セグメントに分割されます。並列システムの場合、一部のシステムは認識しておらず、他のシステムは認識している可能性があります。また、複数のレベルの並列処理とマージが可能な場合もあります。それらに対処するには、非常に濃いお茶が必要になるかもしれません。

イベントの処理

並行モデルを構築するには、どのイベントを処理する必要があるかを決定する必要があります。これは、並行モデルの性質によって異なります。過去の状態である場合、その時点までのすべてのイベントが必要です。これは、メインシステムのイベントログをクエリするだけで完了できます。イベントログの素晴らしい点は、それらが不変のオブジェクトのシーケンスであるため、コピーの作成が簡単である(ただし、遅くなる可能性がある)ことです。

仮想状態である場合は、処理するイベントストリームに代替イベントを定義して注入する方法が必要です。

次に、ライブシステムのイベントを処理するのとまったく同じ方法で、選択したイベントを処理します。すべてが適切に配置されている場合、この時点でシステムに変更を加える必要はありません。定期的な処理を実行するだけです。最後に、アプリケーションの状態が表示され、通常のメカニズムを使用して再度クエリできます。

並行モデルを使用する際の複雑な問題の一つは、同一性危機に陥りやすいことです。通常、モデルがある場合、モデル内のエンティティと、永続ストアおよび現実世界との間に明確な対応関係があります。モデル内のグレゴールオブジェクトは、現実のグレゴールに対応し、そのオブジェクトは1つしかありません。並行モデルを使用すると、複数のグレゴールが存在する状況に陥る可能性があります。それぞれの並行モデルに1つずつです。これは恐ろしいだけでなく、混乱も招きます。結果として、特に並行モデルを埋め込んでいる場合は、並行モデルを操作する際に、どのオブジェクトを保持するかについて非常に注意する必要があります。最も混乱しない方法は、リポジトリを切り替えるときにすべてのオブジェクトを破棄することです。別の方法は、すべてのエンティティに、そのエンティティが取得されたリポジトリへの参照を保持させることです。並行システムは、単一システム内ではこの問題を回避しますが、並行実行の結果をまとめる場合は、この問題に遭遇する可能性があります。

最適化

並行モデルを作成する概念的な概要から、それらの作成が非常に計算コストの高いことがわかります。特に大量のイベントがある場合、最初からすべてのイベントを処理するには時間がかかるでしょう。バージョン管理システムは、しばらくの間この問題に対処する必要があり、この負担を軽減するためのいくつかの方法を考案してきました。

1つは、最初から始める必要がないということです。さまざまな時点でのアプリケーション状態のスナップショットを取得し、最新の妥当なスナップショットを並行モデルの開始点として使用できます。最新の妥当なスナップショットは、イベントストリームで何か異なることを行う前の最後のスナップショットです。履歴並行モデルの場合、これは並行モデルの日付の前の最後のスナップショットになります。仮説並行モデルの場合、これは最初のバリアントイベントの前の最後のスナップショットになります。

いくつかの例を見てみましょう。私は11月17日にいて、9月12日の並行モデルが必要で、毎月の初めにスナップショットを作成します。9月1日のスナップショットから開始し、9月1日から9月12日までに発生したすべてのイベントを処理できます。

私は11月17日にいて、来週の売上急増を調査したいと考えています。この場合、現在のアプリケーション状態からスナップショットを作成し、仮説イベントを追加できます。

私は11月17日にいて、10月に売上が大幅に減少した場合に何が起こったかを調査したいと考えています。10月1日のスナップショットから開始し、バリアントイベントを処理します。

もう1つの最適化は、前方だけでなく後方にも処理することです。これが機能するためには、イベントを可逆にする必要があります。しかし、これを使用すると、10月1日のスナップショットから開始し、10月1日から9月27日までのイベントを反転させることで、9月27日の履歴並行モデルを作成できます。通常、イベントが少ないため、これが高速になります。

処理するイベントを減らすことを考え始めると、選択的なリプレイを考えるのは自然なことです。自分の注文の過去の状態を確認したい場合、自分の注文に影響を与えない限り、他のすべての注文に作用するイベントを無視できる可能性があります。選択的なリプレイを使用すると、処理する必要のあるイベント数を大幅に減らすことができますが、難しいのはイベント間に微妙な相互作用がないことを確認することです。別の注文の履行を処理しないと、システムが輸送に空きがあると誤って認識し、注文の履歴が完全に混乱する可能性があります。システムが複雑になるほど、これらの複雑な相互作用を見つけるのが難しくなります。選択的なリプレイは、前方および後方処理の両方で使用でき、利点とリスクはそれぞれの場合で同じです。

最も一般的なクエリが何かを認識しておくことをお勧めします。Subversionバージョン管理システムは、ほとんどのリクエストがコードの最新バージョンであることを認識しているため、それをスナップショットとして保存し、過去の状態を判断するためにそこから反転イベントを使用します(時々他のスナップショットも使用します)。

これらの最適化の優れた点は、システムのユーザーから完全に隠されているはずであるということです。いつでも、最初から進むことによって並行モデルを作成することを考えることができます。また、その単純な実装から開始し、後でスナップショットを追加することもできます。(可逆イベントは後で追加するのが少し難しく、モデルに影響を与える可能性があります。)

これにより、イベントシーケンスをランダムに生成し、最適化されていない方法とさまざまな最適化された方法を使用して並行モデルを構築するテストへのアプローチが提供されます。並行モデルのアプリケーション状態のいずれかの違いは、さらに調査できる障害を示します。

外部クエリ

イベントソーシングの問題の1つは、外部クエリの結果を覚えておく必要があることです。並行モデルの場合、仮説シナリオを検討すると、追加の問題が発生します。発生しなかったイベントを処理している場合、そのイベントの外部クエリの結果の記録はありません。

この状況では、外部システムの応答として妥当と考えるデータを返すことができるように、ゲートウェイを変更する必要があります。仮説的な記憶クエリでゲートウェイをプログラムすると、これらを仮説シナリオの設定の一部としてキャプチャできます。

使用時期

並行モデルは、履歴および代替状態を処理する1つの方法です。別の代替手段は、時間的プロパティ時間的オブジェクト有効性などのパターンを使用して、この情報をモデル自体に埋め込むことです。

並行モデルを使用する大きな利点の1つは、これらのパターンの複雑さをモデルから取り除くことです。モデルを単純なスナップショットモデルにすることに集中でき、これらの複数の時間と視点を完全に無視できます。もう1つの強みは、これらの構成をすべての場所に配置するのが大変な作業であり、すべての部分が複雑さを増すことです。そのため、どこに配置するかを選択する必要があります。どこかに配置することを怠ると、行き詰まります。後で追加する必要があるか、データが永久に失われるため、チャンスが得られない可能性があります。並行モデルを使用すると、どこでも時間的な動作を取得できます。並行モデルを使用するコストを一度支払うだけで、モデル全体がメリットを享受できます。

このような強みには、マイナス面も伴います。1つ目は、イベントソーシングの前提条件が必要であり、モデルに独自の制約と複雑さが加えられます。2番目の問題は処理時間です。並行モデルのすべてのクエリには、イベント処理が必要です。スナップショットやその他の最適化でできることは限られています。

ほとんどのパターンと同様に、完全にどちらか一方というわけではなく、並行モデルを一般的なメカニズムとして使用し、特定の一般的なリクエストのために一部の場所で時間的プロパティを使用することは十分に可能です。また、イベントソーシングは必要ですが、最初から並行モデルを持つ必要はありません。ただし、イベントソーシングを使用してシステムを構築する場合は、後で役立つ場所に並行モデルを簡単に追加できます。

例:出荷時間クエリ(C#)

この例は、イベントソーシング用に開発した、出荷、貨物、およびポートの単純な例に基づいています。結果として、この例を深く掘り下げる前に、その例に精通しておく必要があります。ドメインロジックコードはほぼ同じです。

図1:出荷例のドメインオブジェクト。

並行モデルの複雑さの多くは、一時的な並行モデルとアクティブなデータベースの操作に関するものであるため、この例にデータベースを導入しました。データベースへのマッピングには、一般的なNHibernateオブジェクトリレーショナルマッパーを使用しています。実際にはあまり面白くないマッピングの詳細については説明しません。代わりに、リポジトリによってどのようにラップされ、並行モデルのリポジトリとどのように交換されるかに焦点を当てます。

ベースの例と同様に、ドメインモデルへのすべての変更はイベントによって処理されます。船が港に到着すると、到着イベントによって記録されます。

class ArrivalEvent...

  string _port;
  int _ship;  
  internal ArrivalEvent (DateTime occurred, string port, int ship) : base (occurred) {
    this._port = port;
    this._ship = ship;
  } 
  internal Port Port {get {return Port.Find(_port);}}
  internal Ship Ship {get {return Ship.Find(_ship);}}
  
  internal override void Process() {
    Ship.HandleArrival(this);
  }

ベースの例との違いは、この場合、ポートと船は識別子として単純な値で示されていることです。この値は、データベース内の主キーに対応していますが、任意のキーを使用できます。ただし、プロパティは実際のドメインオブジェクトを利用します。常に正しいドメインオブジェクトを取得するために、ドメインオブジェクトクラスで定義されたファインダーメソッドを使用してデータベースからドメインオブジェクトをプルします。

class Port...

  public static Port Find(string s) {
    return (Port) Registry.DB.Find(typeof (Port), s);
  }

ファインダーメソッドは、リポジトリに委任します。ベースケースでは、このリポジトリはNHibernateによってマッピングされたドメインオブジェクトを含むデータベースです。したがって、リポジトリコードはNHibernate APIを使用して、データベース(またはNHibernateのキャッシュ)からオブジェクトをプルします。

class DB...

  public object Find(Type type, object key) {
    return _hibernateSession.Load(type, key);
  }

データマッパーに基づいたほとんどのデータソースと同様に、NHibernateはUnit of Workを使用して、操作するオブジェクトを追跡します。結果として、オブジェクトにデータベースに保存するように指示することはありません。代わりに、Unit of Workをコミットすると、メモリ内でどのオブジェクトが変更されたか、およびそれらをどのように書き出すかを把握します。

この例では、イベントプロセッサでこのトランザクションラッピングを実行します。イベントプロセッサは次のようになります。

class EventProcessor...

  public void Process(DomainEvent e) {
    Registry.DB.BeginTransaction();
    try {
      e.Process();
      InsertToLog(e);
      Registry.DB.Commit();
    } catch (Exception ex) {
      Registry.DB.Rollback();
      LogFailedEvent(e, ex);
    }
  }

これは、Unit of Workをラップするのに常に最適な場所であるとは限りません。それはアプリケーションの性質に依存します。しかし、これは私の例では十分に機能し、また、一時的なクエリを処理するために永続的なリポジトリを簡単に切り替える方法という問題も提示します。

テストケースを通じてこれを見てみましょう

class Tester...

  [Test] 
  public void TemporalQueryForShipsLocation() {
    eProc.Process(new ArrivalEvent(new DateTime(2005,11,2), la, kr));
    eProc.Process(new DepartureEvent(new DateTime(2005,11,5), la, kr ));
    eProc.Process(new ArrivalEvent(new DateTime(2005,11,6), sfo, kr));
    Assert.AreEqual(sfo, Ship.Find(kr).Port.Code);
    eProc.SetToEnd(new DateTime(2005,11,2));
    Assert.AreEqual(la, Ship.Find(kr).Port.Code);
  }

ここでの重要なメソッドはSetToEndです。このメソッドは、インメモリリポジトリを使用するようにリポジトリを変更し、イベントログを再処理して、その日の最後のイベントまでイベントが再生されるようにします。これにより、11月2日の並行モデルが作成されます。

class EventProcessor...

  IList log;
  public void SetToEnd(DateTime date) {
    SynchronizeLog();
    IRepository temporalRepository = new MemoryDB();
    Registry.enableAlternateRepository(temporalRepository);
    foreach (DomainEvent e in log) {
      if (e.Occurred > date) return;
      e.Process();
    }
  }

時間クエリを実行するために、プロセッサはデータベースから完全に切り離されます。この場合、プロセッサはイベントログの独自のコピーを保持します。データベースから離れる前に、データベースに書き込まれたすべてのイベントの完全な記録を持つように、そのログを永続的なログと同期します。

ログが同期されたら、完全にインメモリである新しいリポジトリを作成します。これは、SQLを引き続き使用できるようにする埋め込みインメモリデータベースによってバックアップできます。また、リポジトリインターフェイスを満たす手書きのものにすることもできます。これは非常に単純な例なので、ハッシュテーブルをいくつか使用しました。

class MemoryDB...

  public object Find(Type type, object key) {
    object result = this[type][key];
    if (null == result) 
      throw new ApplicationException ("unable to find: " + key.ToString());
    return result;
  }
  private IDictionary store = new Hashtable();
  private IDictionary this[Type index] {get {return (IDictionary)store[index];}}

インメモリリポジトリは、イベントを処理する前のデータベースと同じ初期状態(この場合はポートと船の参照データを保持)に初期化する必要があります。

class MemoryDB...

  public void Initialize() {
    store[typeof(Ship)] = new Hashtable();
    store[typeof(Cargo)] = new Hashtable();
    store[typeof(Port)] = new Hashtable();
    Insert(new Port("SFO", "San Francisco", "US"));
    Insert(new Port("LAX", "Los Angeles", "US"));
    Insert(new Port("YVR", "Vancouver", "CA"));
    Insert (new Port("XXX", "out to sea", "XX"));
    Insert(new Ship("King Roy", 1));
  }

一時リポジトリを設定したら、レジストリに実際のリポジトリの代わりに使用を開始するように指示します。

class Registry...

  internal static void enableAlternateRepository(IRepository arg) {
    instance._repository = arg;
    arg.Initialize();
  }

これで、ドメインモデル内のレジストリへのすべての呼び出しで、インメモリレジストリが使用されます。この設計では、NHibernateが通常データベースリポジトリ内に配置されるため、並行モデルにはhibernateをまったく使用しません。オブジェクトはメモリに表示され、リポジトリによってそこに保持されます。

インメモリリポジトリがセットアップされたら、ログから、対象日以前に発生したすべてのイベントを順番に処理します。完了すると、リクエストされた日の終わり時点での世界の状態を表すインメモリリポジトリが作成されます。ドメインオブジェクトに対するクエリは、その日付を反映します。

通常のデータベースに戻すには、一時的なインメモリリポジトリを通常のリポジトリ接続と入れ替えるだけです。

class Registry...

  internal static void restoreBaseRepository() {
    instance._repository = instance._baseRepository;
  }

例:ベースモデルとの比較(C#)

パラレルモデルの多くの用途では、一度に1つのモデルのみを操作します。更新を含む一般的な処理はベースモデルで行い、履歴クエリや仮説クエリ用のパラレルモデルを構築します。常に1つのパラレルモデルのみが使用されます。このスキームは比較的シンプルで、多くのSFプロットを悩ませるパラレルユニバース間の同一性に関する問題を回避します。

しかし、2つを組み合わせることが理にかなう場合もあります。出荷の例のつまらないユーモアを覚えていれば、書籍販売業者がカナダを非常に恐れており、「eh」でテキストを汚染するリスクがあることを知っているでしょう。時々、特に厄介な「eh」の感染が発生すると想像してみましょう。そのような感染が発生した港に停泊している船の貨物は、高リスクとしてマークする必要があります。もちろん、このようなことは当日には判明しないため、その日に港に何があったかを把握する必要があります。

この問題を表現するためのテストケースを以下に示します。

class Tester...

  [Test]
  public void HighRiskDayFlagsAllCargo() {
    eProc.Process(new RegisterCargoEvent(new DateTime(2005,1,1), "UML Distilled", "UML", "LAX" ));
    eProc.Process(new RegisterCargoEvent(new DateTime(2005,1,1), "Planning XP", "PXP", "LAX" ));
    eProc.Process(new RegisterCargoEvent(new DateTime(2005,1,1), "Analysis Patterns", "AP", "LAX" ));
    eProc.Process(new RegisterCargoEvent(new DateTime(2005,1,1), "P of EAA", "eaa", "LAX" ));
    eProc.Process(new ArrivalEvent(new DateTime(2005,11,2), la, kr));
    eProc.Process(new LoadEvent(new DateTime(2005,5,11),"PXP", 1));
    eProc.Process(new LoadEvent(new DateTime(2005,5,11),"AP", 1));
    eProc.Process(new ArrivalEvent(new DateTime(2005,11,9), yvr, kr));
    eProc.Process(new ArrivalEvent(new DateTime(2005,11,12), la, kr));
    eProc.Process(new ContagionEvent(new DateTime(2005,11,10), yvr));
    Assert.IsTrue(Cargo.Find("PXP").IsHighRisk, "PXP should be high risk");
    Assert.IsTrue(Cargo.Find("AP").IsHighRisk, "AP should be high risk");
    Assert.IsFalse(Cargo.Find("UML").IsHighRisk, "UML should NOT be high risk");
    Assert.IsFalse(Cargo.Find("eaa").IsHighRisk, "UML should NOT be high risk");
    Assert.IsFalse(Cargo.Find(refact).IsHighRisk, "UML should NOT be high risk");
  }

新しいイベントで感染症を記述します。

class ContagionEvent...

  internal class ContagionEvent : DomainEvent
  {
    string _portID;
    public ContagionEvent(DateTime occurred, string port) : base(occurred) {
      this._portID = port;
    }
    Port Port {get {return Port.Find(_portID);}}
internal override void Process() {
  Registry.EventProcessor.SetToEnd(Occurred);
  ArrayList infectedCargos = new ArrayList();
  foreach (Ship s in Port.Ships) infectedCargos.AddRange(s.Cargos);
  Registry.restoreBaseRepository();
  foreach (Cargo c in infectedCargos) {
    Cargo actualCargo = Cargo.Find(c.RegistrationCode);
    actualCargo.IsHighRisk = true;
  }
}

この場合、イベントのprocessメソッドにかなり多くの動作があることに気づくでしょう。これは、ドメインモデルをパラレルモデルについて知らないようにしたいと考えたためです。したがって、イベントは感染の日付のための一時的なパラレルモデルを作成し、影響を受ける貨物を調べるクエリを実行し、世界をベースモデルに戻し、更新を渡す必要があります。ドメインモデルは、各パラレルモデル内でロジックを実行しますが、それほど多くはありません。

真剣に検討すべきもう1つの方法は、貨物を高リスクとしてマークするための新しいイベントを作成することです。この場合、感染イベントは一時的なパラレルモデルで影響を受ける貨物を見つけ、これらの貨物を高リスクとしてマークするイベントを作成します。この2番目のイベントはベース状態で実行されます。これを書いている時点では、どちらのアプローチが適切か確信がありません。