統合ソリューションを通ってくる多くのメッセージは、複数の要素を含んでいる。 たとえば、顧客の注文は、通常、ひとつ以上の商品行を含んでいる。 Content-Based Router(230)の説明を参照すると、各商品行は、異なる在庫システムにより処理される必要があるかも知れない。 したがって、各注文に含まれる商品を個別に扱って、完全に注文を処理するためのアプローチを探す必要がある。
メッセージが複数の要素を含んでいて、別々の方法で処理されねばならないとき、我々は、どのようにそのメッセージを処理すればよいのか。
このルーティング問題の解決策は十分に汎用的であるべきで、さまざな要素の数と型に対処できるべきである。 たとえば、ある注文はどんなに多くの商品数でも含めることができなければならないので、商品数が固定していると仮定するといった解決策をとりたくはない。 また、そのメッセージが踏んでいる商品の型についての多すぎる仮定をおきたくはない。 たとえば、Widgets & Gadgets 'R Us が明日から書籍の販売を開始するときには、全体のソリューションへの影響を最小化したい。
また、注文商品をコントロールし二重処理や未処理をなくし続けたい。 たとえば、Publish-Subscribe Channel(106) を用いて、各受注管理システムへ注文全体を送って、それが処理できる商品を取り出させるようにすることが出来る。 このアプローチには Content-Based Router(230) で説明したのと同じ不利な点がある。つまり、それぞれの商品の出荷が、二重処理されたり未処理だったりすることを防ぐのが非常に難しい。
この解決策は、ネットワークリソースを効率的に使えるようになっているべきである。 ほんの一部を処理すればよいのに、注文全体を含むメッセージを各システムに送ることは、特に送付先が増えたときに、追加のメッセージトラフィックを発生させる。
メッセージ全体を何度も送るのを防ぐため、元のメッセージを、在庫管理システムと同じだけ多くのメッセージに分割することができる。 そして、各メッセージは、特定のシステムが処理できる商品行のみを含む。 このアプローチは、メッセージを分割して個別のメッセージをルーティングすることを除いて、Content-Based Router (230) と似ている。 このアプローチは効率的だが、特定の商品型と送付先への関連付けに関する知識とこの解決策が結びつくことになる。 もし、ルーティングルールを変えたいとしたらどうなるであろうか。 これを、今度は、より複雑な「商品ルーター」コンポーネントに変えなければらない。 複数の機能をひとつにまとめるのではなく、処理を分割するために、明確に定義され、組み立て可能なコンポーネント用意するという、以前、Pipe and Filter (70) アーキテクチャを使った。そして、ここでも同様にこのアーキテクチャを活用することが出来るべきである。
合成メッセージを、ひとつの商品に関連するデータをそれぞれ含んでいる一連の個別メッセージに分解するために、Splitterを使用する。
その Splitter は、入力メッセージ中のひとつの要素ごとに、ひとつのメッセージ(もしくは、要素の部分集合)を発行する。 多くの場合において、いくつかの共通要素を各結果メッセージに繰り返し入れたい。 結果として得られる子メッセージを自己完結型にし、それぞれの子メッセージの処理をステートレスに出来るようにするために、それらの拡張要素が必要とされるかもしれない。 それは、また、関連する子メッセージを後で統合(reconciliation)できるようにする。 たとえば、顧客の発注情報といった、すべての関連するエントリーと商品を受注に正しく関連づけなおせるように、各商品発注メッセージは、受注番号のコピーを含まねばならない。(図を参照)
共通データ要素を各子メッセージにコピーする
以前説明したとおり、多くの企業統合システムは、メッセージデータを木構造で保存している。 木構造の美しさは、再帰的であるということである。 あるノードの下の各子ノードは、他の部分木の根元である。 このことは、あるメッセージ木を処理するのに、その一片を抽出して処理することで、全体が処理できるようする。 もし、メッセージ木を使うならば、特定ノード以下のすべての子供に対し、各子ノードを一つのメッセージとして送るようにすることで、Splitterは簡単に構築することができる。 そのようなSplitterの実装は完全に汎用的である。なぜなら、それは、子要素の数や型について何も仮定しないからである。 多くの商用EAIツールは、iteratorとかsequencerといた用語で、こういったタイプの機能を提供している。 混乱が入り込むことを減らすために、ベンダー専門用語を排除することを試みるため、このスタイルのSplitterをiterating Splitterと呼ぶことにする。
しかしながら、Splitterは、要素を繰り返しにのみ利用されるわけではない。 とある大きなメッセージは、処理の単純化のために、個別のメッセージに分割されるかも知れない。 たとえば、多くのB2B情報交換標準では、とても包括的なメッセージ形式を定義している。 その巨大なメッセージは、しばしば、「コミッティーによる設計」の結果であり、メッセージの多くの部分はめったに使われないかも知れない。 多くの場合において、それらのメガメッセージを、大きなメッセージの特定の部分を中心においた個別のメッセージに分割することは、有用である。 これは、より小さなメッセージをメガメッセージの一部のみを処理するコンポーネントにルーティングできるため、次に続く変換を開発するのをとても簡単にし、また、ネットワーク帯域幅を節約もする。 結果メッセージはしばしば、同じチャネルではなく異なるチャネル発行される。なぜなら、それらは異なるサブタイプのメッセージを表現しているからである。 このシナリオにおいては、結果メッセージの数は一般的には固定であるのに対して、より一般的な Splitter は可変であることを仮定している。 このスタイルの Splitter を区別するために、それを static Splitter と呼ぶことにしよう。 static Splitter は、Content Filters (342) の集合が後ろに続いている放送チャネル(broadcast channel)と機能的に同等である。
Static Splitter は、大きなメッセージを、固定数の小さなメッセージに分割する。
いくつかのケースにおいて、メッセージのトレーサビリティを改善し、Aggregator(268)の処理を単純化するために、子メッセージにシーケンス番号を付与することは有用である。 また、個別のメッセージからの処理結果を元のメッセージに関連付け戻せるように、元の(結合された)メッセージへの参照を付与することも有用である。 この参照は、Correlation Identifier(163)として働く。
もし、メッセージエンベロープ(Envelope Wrapper[330]を参照)が使われるならば、各新しいメッセージのメッセージエンベロープがメッセージインフラに準拠するように、それが提供されるべきである。 たとえば、もし、そのインフラがメッセージヘッダー中にタイムスタンプを運ぶようにメッセージに要求するならば、元のメッセージのタイムスタンプが各子メッセージのヘッダーに伝播するようにする。
例: Splitting an XML Order Document in C#
多くのメッセージングシステムは XML メッセージを使用している。 たとえば、入力メッセージ「注文」が下記のようだと仮定してみよう。
<order> <date>7/18/2002</date> <ordernumber>3825968</ordernumber> <customer> <id>12345</id> <name>Joe Doe</name> </customer> <orderitems> <item> <quantity>3.0</quantity> <itemno>W1234</itemno> <description>A Widget</description> </item> <item> <quantity>2.0</quantity> <itemno>G2345</itemno> <description>A Gadget</description> </item> </orderitems> </order>
このスプリッターが、受注情報を個別の商品受注に分割するようにしたい。 この例示ドキュメントでは、このスプリッターは以下の2つのメッセージを生成することになる。
<orderitem> <date>7/18/2002</date> <ordernumber>3825968</ordernumber> <customerid>12345<customerid> <quantity>3.0</quantity> <itemno>W1234</itemno> <description>A Widget</description> </orderitem> <orderitem> <date>7/18/2002</date> <ordernumber>3825968</ordernumber> <customerid>12345<customerid> <quantity>2.0</quantity> <itemno>G2345</itemno> <description>A Gadget</description> </orderitem>
各 orderitem メッセージは、元の order メッセージから、注文日付、注文番号、商品番号が補強されている。 その、顧客IDと注文日付を含んでいることは、そのメッセージを自己完結型にさせ、メッセージ利用者が個別メッセージをまたがったコンテキストを保存しなければならないことを防ぐ。 もし、そのメッセージがステートレスサーバにより処理されるならば、これは重要である。 ordernumberフィールドを追加することは、各商品情報を後で再凝集するために必要である。(Aggregator[268]を参照) この例では、ある注文の中に特定の商品の列があることは、その注文を完結するのに適切ではない、と仮定している。なので、商品番号を含んではいけなかった。(訳注:意味不明。もしかして、customerid のことを言っている!?)
では、C# で、Splitterのコードがどんな感じか見てみましょう。
class XMLSplitter { protected MessageQueue inQueue; protected MessageQueue outQueue; public XMLSplitter(MessageQueue inQueue, MessageQueue outQueue) { this.inQueue = inQueue; this.outQueue = outQueue; inQueue.ReceiveCompleted += new ReceiveCompletedEventHandler(OnMessage); inQueue.BeginReceive(); outQueue.Formatter = new ActiveXMessageFormatter(); } protected void OnMessage(Object source, ReceiveCompletedEventArgs asyncResult) { MessageQueue mq = (MessageQueue)source; mq.Formatter = new ActiveXMessageFormatter(); Message mesage = mq.EndReceive(asyncResult.AsyncResult); XmlDocument doc new XmlDocument(); doc.LoadXml((String)message.Body); XmlNodeList nodeList; XmlElement root = doc.DocumentElement; XmlNode date = root.SelectSingleNode("date"); XmlNode ordernumber = root.SelectSingleNode("ordernumber"); XmlNode id = root.SelectSingleNode("customer/id"); XmlElement customerid = doc.CreateElement("customerid"); customerid.InnerText = id.InnerXml; nodeList = root.SelectNodes("/order/orderitems/item"); foreach (XmlNode item in nodeList) { XmlDocument orderItemDoc = new XmlDocument(); orderItemDoc.LoadXml("<orderitem/>"); XmlElement orderItem = orderItemDoc.DocumentElement; orderItem.AppendChild(orderItemDoc.ImportNode(date, true)); orderItem.AppendChild(orderItemDoc.ImportNode(ordernumber, true)); orderItem.AppendChild(orderItemDoc.ImportNode(customerid, true)); for (int i=0; i < item.ChildNodes.Count; i++) { orderItem.AppendChild(orderItemDoc.ImportNode(item.ChildNodes[i], true)); } outQueue.Send(orderItem.OuterXml); } mq.BeginReceive(); } }
コードの多くは XML の処理を中心においている。 XMLSplitter は、他のルーティング例と同様に Event-Driven Cosumer (498) 構造を使用している。 各入力メッセージは、操作のために、本体を XML ドキュメントに変換する OnMessage? メソッドを呼び出す。 まず、注文ドキュメントから関連のある値を抽出する。 そして、各 <item> 子要素を繰り返し処理する。 XPath式 /order/orderitems/item を指定する事により、それをする。 単純な XPath式は、ファイルパスととてもよく似ている。それは、そのパスで指定された要素名とマッチするドキュメント木を下っていく。 各 <item> ごとに、他のものと item の子ノードから持ち越されたフィールドをコピーすることにより、新たな XML ドキュメントを構築する。
例: Splitting an XML Order Document in C# and XSL
XMLノードと要素を手で操作する代わりに、入力XMLを求められる形式に変換して、変換されたXMLドキュメントから出力メッセージを生成するために、XSL ドキュメントを生成することも出来る。 ドキュメントがよく変化する場合には、これは、より、メンテナンス性が高い。 やらなければならないこと全部は、C#コードを一切変えることなく、XSL変換を変えることである。
新しいコードは、入力ドキュメントを中間ドキュメント形式に変換するために、XslTransform? クラスで提供された Transform メソッドを使用している。 その中間ドキュメント形式は、各結果メッセージごとに、一つの子要素 orderitem を持っている。 そのコードは単純にすべての子要素を横断して、各要素ごとにひとつのメッセージを発行する。
class XSLSplitter { protected MessageQueue inQueue; protected MessageQueue outQueue; protected String styleSheet = "..\\..\\Order2OrderItem.xsl"; protected XslTransform xslt; public XSLSplitter(MessageQueue inQueue, MessageQueue outQueue) { this.inQueue = inQueue; this.outQueue = outQueue; xslt = new XslTransform(); xslt.Load(styleSheet, null); outQueue.Formatter = new ActiveXMessageFormatter(); inQueue.ReceiveCompleted += new ReceiveCompletedEventHandler(OnMessage); inQueue.BeginReceive(); } protected void OnMessage(Object source, ReceiveCompletedEventArgs asyncResult) { MessageQueue mq = (MessageQueue)source; mq.Formatter = new ActiveXMessageFormatter(); Message message = mq.EndReceive(asyncResult.AsyncResult); try { XPathDocument doc = new XPathDocument (new StringReader((String)message.Body)); XmlReader reader = xslt.Transform(doc, null, new XmlUrlResolver()); XmlDocument allItems = new XmlDocument(); allItems.Load(reader); XmlNodeList nodeList = allItems.DocumentElement. GetElementsByTagName("orderitem"); foreach (XmlNode orderItem in nodeList) { outQueue.Send(orderItem.OuterXml); } } catch (Exception e) { Console.WriteLine(e.ToString()); } mq.BeginReceive(); } }
簡単に編集してテスト出来るように、XSLドキュメントを別のファイルから読み込む。 また、コードを再コンパイルすることなく、Splitterの振る舞いを変えることを可能とする。
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" /> <xsl:template match="/order"> <orderitems> <xsl:apply-templates select="orderitems/item"/> </orderitems> </xsl:template> <xsl:template match="/order"> <orderitem> <date> <xsl:value-of select="parent::node()/parent::node()/date"/> </date> <ordernumber> <xsl:value-of select="parent::node()/parent::node()/ordernumber"/> </ordernumber> <customerid> <xsl:value-of select="parent::node()/parent::node()/customer/id"/> </customerid> <xsl:apply-templates select="*"/> <orderitem> </xsl:template> <xsl:template match="*"> <xsl:copy> <xsl:apply-templates select="@* | node()"/> </xsl:copy> </xsl:template> </xsl:stylesheet>
XSLは宣言的な言語なので、かなりのXMLを自分自身で書いた(もしくは、[Tennison]のような、良いXSL本を読んだ)ことがないと、うまく作るのは簡単ではない。 このXSL変換は order 要素のすべての存在を探す。(このドキュメントには、一つある。) 一旦、この要素を見つけたら、それは、出力ドキュメントとして、新たなルート要素を生成する。(すべてのXMLドキュメントは単一のルート要素を持たなければならない。)そして、入力ドキュメントの orderitems 要素の中にあるすべての item 要素に取り掛かる。 このXSLは、見つかった各 item ごとに、新しいテンプレートを指定する。 このテンプレートは、order 要素(その item の親の親である) order 要素から、date と ordernumber と customerid 要素をコピーする。そして、item からすべての要素を追加する。 結果ドキュメントは、入力ドキュメント内の各 item 要素ごとに、ひとつの orderitem 要素を持っている。 C#コードにとって、その要素を繰り返して、それをメッセージとして発行するのを簡単にする。
その2つの実装がどのように実行されるのかに興味がある。 実際に手早く非科学的な性能テストを実行することにした。 5,000個の注文メッセージを入力キューに単純に入力し、Splitterを開始し、10,000個の商品メッセージが出力キューに届く時間を計測した。 ローカルメッセージキューを使用している一つのマシンの単一のプログラム内にこれをすべて入れて実行した。 要素を抽出するのにDOMを使うXMLSplitterには7秒かかり、XSLのSplitterには5.3秒かかった。 必ずかかる時間として、入力キューの一つのメッセージを消費し、出力キューに同じメッセージを2回出力するダミープロセッサーが5000個のメッセージを処理するに2秒以下かかる。 この時間には、ダミープロセッサーの5000個のメッセージの消費と、10000個のメッセージの生成、そして、そのプロセッサーが発行する10000メッセージを消費するテスト装置、が含まれている。 そして、XSL操作は、「手で」要素を取り回すよりも、わずかだか良り効率的であるように見える。(もし、必ずかかる時間を差し引くなら、XSLは約35パーセント早い。) プログラムの両方とも、最大の性能のを出すようにすることが出来ると知っているが、それらを並べて実行するのを見るのは、興味深かった。