はじめに
前の記事『SQLXMLとシリアル化を利用してSQL Serverからオブジェクトを取得する』を発表した後、私は、同記事で設計したスキーマ定義ファイルと、SQLXMLに含まれるUPDATEGRAMという別の技法を使用して、オブジェクトをデータベースに保存し直す方法について調べました。
この記事では、マイクロソフトが設計したこの技法について説明します。そして、シリアル化とカスタムクラス属性を使用し、SQL Serverに送信して自動生成文を実行できる適切なXMLストリームを形成する、UPDATEGRAM
クラスを作成します。
UPDATEGRAM機能
まず最初に、Microsoft SQLXMLのUPDATEGRAM機能の概要を紹介します。UPDATEGRAM機能の詳細は、オンラインヘルプ(マシンにSQLXMLライブラリがインストールされている場合)またはMSDNサイトで見ることができます。この概念を要約すると、UPDATEGRAMは、XMLドキュメントであると共にXMLスキーマ定義であり、SQLXMLライブラリによって使用され、レコードを更新、挿入、および削除するコマンドの作成と実行をSQL Serverにおいて自動化するものと言えます。
各UPDATEGRAMは、主に、一連のSYNC
要素によって形成されます。それぞれは、同じトランザクションコンテキストで実行される操作のセットです。各SYNC
要素は、実行される操作セットごとにBEFORE
要素とAFTER
要素を含むことができます。レコードを配置したり作成したりするにはBEFORE
要素を使用し、レコードへの変更を保持するにはAFTER
要素を使用します。空のBEFORE
要素を指定すると、挿入が実行されます。一方、空のAFTER
は、削除操作に相当します。両方が存在する場合は、「UPDATE AFTER where BEFORE」のような状態が生成されますが、この記事ではこの状態を扱います。
データ更新用フォームの追加
さて、前の記事のWindowsアプリケーションに戻り、指定のOrder
のプロパティを変更し、データを更新してSQL Serverに戻す新しいフォームを追加しましょう。
グリッドフォームからフォームを開くためのDoubleClickイベントハンドラを追加し、次のコードを追加します。
private void grd_orders_DoubleClick(object sender, EventArgs e) { Form f = new OrderForm( this.orders[this.grd_orders.CurrentRowIndex]); f.Visible=true; }
Loadイベントの中で、Order
をコントロールにバインドします。
private void OrderForm_Load(object sender, System.EventArgs e) { this.lblOrderNum.Text = this.order.OrderID.ToString(); //we don't want null dates, //instead the control will be set to current or default if(this.order.OrderDate!=DateTime.MinValue) this.dtpOrderDate.Value = this.order.OrderDate; if(this.order.RequiredDate!=DateTime.MinValue) this.dtpRequiredDate.Value = this.order.RequiredDate; this.txtShipAddress.Text = this.order.ShipAddress; this.txtShipCity.Text = this.order.ShipCity; this.txtShipName.Text = this.order.ShipName; this.txtShipZip.Text=this.order.ShipPostalCode; this.txtShipCountry.Text = this.order.ShipCountry; }
[Save]ボタンの実装
Order
を編集できる状態になったので、[Save]ボタンの実装に進みましょう。Form
内のコントロールから値を取得し、それをOrder
オブジェクトに設定するメソッド(getChanges()
など)を作成する必要があります。我々の目標は、古いOrder
をBefore
ノード、変更後のOrder
をAfter
ノードとするUPDATEGRAMを生成し、SQL Serverに送信して、更新を自動的に行うことです。
まずに、Before状態を取得するメソッドを定義する必要があります(オブジェクトの現在の状態がAfter状態になります)。そこで、バイナリシリアル化を使用して、オブジェクトの複製と比較を行うことにしました。これは、プロジェクト内のエンティティの基本クラスで使用される、私が作成した小さなヘルパークラスです。
public class CloneHelpers { public static object DeepClone(object source) { MemoryStream m = new MemoryStream(); BinaryFormatter b = new BinaryFormatter(); b.Serialize(m, source); m.Position = 0; return b.Deserialize(m); } public static bool DeepEquals(object objA,object objB) { MemoryStream serA = serializedStream(objA); MemoryStream serB = serializedStream(objB); if(serA.Length!=serA.Length) return false; while(serA.Position<serA.Length) { if(serA.ReadByte()!=serB.ReadByte()) return false; } return true; } public static MemoryStream serializedStream(object source) { MemoryStream m = new MemoryStream(); BinaryFormatter b = new BinaryFormatter(); b.Serialize(m, source); m.Position = 0; return m; } }
次に、基本のビジネスクラスを定義しましょう。
[Serializable] public abstract class BaseBusinessEntity { public object DeepClone() { return CloneHelpers.DeepClone(this); } public bool DeepEquals(object obj) { return CloneHelpers.DeepEquals(this,obj); } } [Serializable] public abstract class BaseBusinessEntityCollection : CollectionBase { public object DeepClone() { return CloneHelpers.DeepClone(this); } }
OrderForm
では、操作するエンティティの初期状態を定義するコードとプロパティを設定できます。
public OrderForm(OrdersMgmt.Order ord) { //get the order from the grid form this.order=ord; //set the initial state this.orderBefore = (OrdersMgmt.Order) ord.DeepClone();
この時点で、次のように考える方がいるかもしれません。初期状態のオブジェクトをXMLへとシリアル化してBefore
ノードを作成し、オブジェクトのプロパティが変更されたら、オブジェクトをXMLへ再びシリアル化してAfter
ノードを取得する、というやり方です。
ここで、UPDATEGRAMの生成を抽象化してみましょう。そして、XMLストリームを正しく形成および実行するプロパティを保持するクラスを設計します。
このクラスを最も重要なプロパティであるXSDファイルでインスタンス化します。
public updategram(string schemaFile) { //set the current schema to use //when sending updategram to SQLSERVER this.schemaFilePath = schemaFile; //initialize the state of the updategram //and open the xml document this.InitUpdgDocument(); this.syncCollection = new Hashtable(); }
InitUpdgDocument()
でXMLドキュメントが開始します(ここでは話を簡単にするためにdocument
オブジェクトを使用していますが、自分の技術レベルに合わせて自由に最適化できます)。
private void InitUpdgDocument() { if(this.updgDocument==null) { //create empty updategram updgDocument = new XmlDocument(); updgDocument .LoadXml("<ROOT xmlns:"+prefix+"=\""+ns+"\"></ROOT>"); } }
syncCollection
は、初期状態をキーとして、かつ、実際の状態を値として、変更する必要があるすべてのオブジェクトを保持しています。UPDATEGRAMとフォームの初期状態が作成されたので、[Save]ボタンをクリックするとどうなるかを確認できます。
private void btnUpdate_Click(object sender, System.EventArgs e) { //get form values and set entity values this.getChanges(); //compare to object and create new sync node if something changed updg.Process(this.orderBefore,this.order); //commit all sync nodes created updg.Commit(); //set new initial state this.orderBefore = (OrdersMgmt.Order) this.order.DeepClone(); //this.Close(); }
getChages()
演算では、コントロールからorder値を設定し、UPDATEGRAMのprocess
メソッドを呼び出してオブジェクトを比較し、2つの状態が異なる場合は新しいキー/値エントリをsyncCollection
に挿入します。この例の場合、操作しているオブジェクトは1つだけですが、複数のオブジェクトの変更も問題なく行われます。すべての更新をコミットできる状態になったら、updategram
のCommit
メソッドを呼び出します。SQL Serverで実際の更新作業が実行されます。Commit
メソッドのコードを見てみましょう。
public void Commit() { //cycle all syncs with original keys foreach(object objOriginal in this.syncCollection.Keys) { //get reference to changed object object objChanged = this.syncCollection[objOriginal]; //initialize a the serializer provider UpdgXSerializer x = new UpdgXSerializer(objOriginal,objChanged); //get the serializer for the before element this.beforeSerializer = x.getUpdgSerializer(UpdategramElement.Before); //get the serializer for the after element this.afterSerializer = x.getUpdgSerializer(UpdategramElement.After); //create the sync element this.createSync(); //create the before element this.BeginUpdate(objOriginal); //create the after element this.EndUpdate(objChanged); } //verify that we have a document to submit if(this.updgDocument==null) return; //create the stream with the xml document MemoryStream ms; ms = new MemoryStream(); this.updgDocument.Save(ms); ms.Position = 0; try { //execute the xml stream XmlReader results = sqlxmlHelper.executeUpdateGram(this.schemaFilePath,ref ms); } catch(Exception ex) { throw new Exception( "Error while committing operations to DB, "+ex.Message,ex); } finally { //clean up after submit ms.Close(); } this.syncCollection = new Hashtable(); updgDocument = null; }
既定のシリアル化では、すべてのOrderLines
が子ノードとして順々に含まれるXMLが形成されます。
<updg:before> <Order> <OrderID>10250</OrderID> ... <OrderLines> <OrderDetail> <Quantity>10</Quantity> <Discount>0</Discount> <Item> <ProductID>41</ProductID> <ProductName>Jack's New England Clam Chowder </ProductName> <UnitPrice>9.65</UnitPrice> </Item> <OrderID>10250</OrderID> <ProductID>41</ProductID> </OrderDetail> <OrderDetail> <Quantity>35</Quantity> <Discount>0.15</Discount> <Item> <ProductID>51</ProductID> <ProductName>Manjimup Dried Apples</ProductName> <UnitPrice>53</UnitPrice> </Item> <OrderID>10250</OrderID> <ProductID>51</ProductID> </OrderDetail>