問題点
SQLXMLに送信されると、次のエラーが発生します。
つまり、ノード(オブジェクト)ごとに、インデックスまたはIDを設定して、updg:id
属性を指定する必要があります。今回の例では、OrderDetail
オブジェクトに読み取り/書き込みプロパティを用意し、このプロパティにXML内での名前と記述方法を定義する属性を持たせるという簡単な方法を採用しています。
[XmlAttribute(AttributeName="id", Namespace="urn:schemas-microsoft-com:xml-updategram")] public int OrderIDKey { get { return this.ProductID; } set { } }
このケースのProductID
は、Order
内のOrderDetail
ごとに一意であるため、IDとして適しています。また、プロパティの読み取り/書き込みを行うには空のset
が必要です。そうでないと、XMLでシリアル化されません。
Order
オブジェクトに対しても同じ操作を行い、複数のOrder
のバッチ更新を可能にします。
[XmlAttribute(AttributeName="id", Namespace="urn:schemas-microsoft-com:xml-updategram")] public int OrderIDKey { get { return OrderID; } set { } }
Order
とOrderDetail
の属性を使用した新しいUPDATEGRAMのXMLを見てみましょう。
<Order updg:id="10250"> <OrderLines> <OrderDetail updg:id="41">
このケースでは、SQL Serverに送信されても何も問題は生じません。
しかし、IT環境では、当初とまったく変わらないものなどあり得ません。そこで、既定のシリアル化が動作しないいくつかのケースと、その問題の解決方法について考えてみましょう。
null値の処理
まず、null
値の処理です。Webフォームの入力フィールドを処理したことがある人ならば誰でも、ユーザーがフィールドをクリアして、null
値ではなく空白が、文字列型、または数値や日付の既定値としてデータベースに保存されるという問題を経験したことがあるでしょう。また、null
プロパティは空の要素でシリアル化されるため、SQLXMLはそのカラムを更新しません。
SQLXMLのNULL処理については、MSDNライブラリで説明されています。XMLルート要素で定義される既定値を使用すると(例ではupdg:nullvalue="|isnull|"
を使用)、その値を含むすべての要素がDBNULLで更新されます。今回の例では、null
化が必要なすべてのプロパティ名をパブリックフィールドに格納するという方法を採用しています。
[XmlArray("nullProps"),XmlArrayItem("prop")] public string[] nullProps;
次のようにして、プロパティ名をリストに追加するメソッドを公開します。
public void nullProperty(string propertyName) { foreach(PropertyInfo pInfo in this.GetType().GetProperties()) { if(pInfo.Name==propertyName) { if(nullProps!=null && Array.IndexOf((Array)nullProps,propertyName)==-1){ string[] tmp = (string[]) nullProps.Clone(); nullProps = new string[tmp.Length+1]; nullProps[0] = propertyName; tmp.CopyTo(nullProps,1); } else nullProps = new string[]{propertyName}; break; } } }
フォームのgetChanges()
メソッドでは、次のようにして、挿入されたnull
値を簡単に処理できます。
if(this.txtShipZip.Text==string.Empty) this.order.nullProperty("ShipPostalCode"); if(this.txtShipCountry.Text==string.Empty) this.order.nullProperty("ShipCountry");
UPDATEGRAMクラスのwriteAfterXML
を見てみましょう。このメソッドでは、After
ノードを作成し、null
値を設定します。
private void writeAfterXML(ref object obj,ref XmlElement after) { MemoryStream ms; StreamReader sr; ms = new MemoryStream(); XSerializer.serialize(ref ms ,obj,this.afterSerializer); sr = new StreamReader(ms); sr.BaseStream.Position = 0; //remove declaration after.InnerXml = sr.ReadToEnd().Remove(0,23); //set null values //get the list of properies to set DBNULL //for each object at any level XmlNodeList list = after.SelectNodes("//*[nullProps]"); foreach(XmlNode node in list) { //select the list of properties XmlNodeList propsToNull = node.SelectNodes("nullProps/prop"); foreach(XmlNode prop in propsToNull) { XmlNode property = node.SelectSingleNode(prop.InnerText); if(property!=null) property.InnerText = UPDG_NULL_CODE; } //remove nullProps node to avoid sqlxml conflict node.RemoveChild(node.SelectSingleNode("nullProps")); } //remove empty elements list = after.SelectNodes("//*[.='']"); foreach(XmlNode node in list) { node.ParentNode.RemoveChild(node); } sr.Close(); ms.Close(); }
writeBeforeXML
では、削除されてnull
ノードになるプロパティだけが問題です。1つのOrder
についてテストを行い、郵便番号と国を削除し、保存すると、結果は次のようになります。
リレーショナルデータの整合性とルックアップ値
もう1つの問題は、リレーショナルデータの整合性とルックアップ値に関するものです。次のコード行で、Order
から先頭のOrderLine
を削除するものとします。
this.order.OrderLines.RemoveAt(0);
AFTERノード内のこの後のXMLシリアル化が、削除されたOrderLine
を失うのは当然ですが、これが原因で、OrderDetail
だけでなく関連するProducts
も削除されるかどうかは定かではありません。関係が適切に定義されている「Northwind」データベースの場合は、次の図のような例外がスローされます。
Product
は別のOrderLine
によって参照され、Product
が削除された場合、連鎖処理は実行されません。これとは別に、関係が存在しない場合は、オブジェクトのこの部分をUPDATEGRAMに含まない方法を見つける必要があります。私は考えて、これらのプロパティを除外するメソッドを作成することにし、オブジェクトのシリアル化をカスタマイズすることにしました。
シリアル化のカスタマイズ
そこで、まず最初に、XMLシリアル化から除外するプロパティをマークする属性を作成しました。
public class LookupAttribute:System.Attribute { public LookupAttribute() {} }
次に、常に確実に必要なプロパティ(プライマリキーや外部キーなど)について考慮して、2番目の属性を作成しました。
public class IntegrityCheckAttribute:System.Attribute { public IntegrityCheckAttribute() {} }
その後、プロパティをOrderDetail
内のProduct
と同じように定義しました。
[Lookup]
public Product Item
また、すべてのプライマリキーと外部キーを、Order
内のOrderID
と同じように定義しました。
[IntegrityCheck] public int OrderID
オブジェクトの状態に応じてマーク付きプロパティをXMLシリアル化から除外する、という機能を持ったオブジェクト用のカスタムシリアライザを作成できます。XmlAttributeOverrides
クラスについては、MSDNの記事を参照してください。
このシリアライザの構成は次のとおりです。
コンストラクタのコードを次に示します。
public UpdgXSerializer(object objBefore,object objAfter) { //hold reference to before state this._Before=objBefore; //hold reference to after state this._After=objAfter; }
UDATEGRAMノードのSerializer
オブジェクトを返すメインの処理は次のとおりです。
public XmlSerializer getUpdgSerializer(UpdategramElement updgElement) { //init. the collection of overrides XmlAttributeOverrides myOverrides = new XmlAttributeOverrides(); Type objType=null; //choose the right object switch(updgElement) { case UpdategramElement.Before: //parse the object to retrive attribute overrides this.parseType(this._Before,ref myOverrides,updgElement); objType = this._Before.GetType(); break; case UpdategramElement.After: this.parseType(this._After,ref myOverrides,updgElement); objType = this._After.GetType(); break; } //advanced serializer with attribute overrides XmlSerializer retVal = new XmlSerializer(objType, myOverrides); return retVal; }
parseType
プライベート呼び出しは、隠す必要があるプロパティを見つけるための処理を開始します。
private void parseType(object source, ref XmlAttributeOverrides xmloverrides, UpdategramElement updgElement) { Type objType = source.GetType(); // Iterate through all the properties of the class foreach(PropertyInfo pInfo in objType.GetProperties()) { //get the attribute overrides for the property getAttributesForProperty( pInfo,ref xmloverrides,objType,updgElement); if(typeof(IList).IsAssignableFrom(pInfo.PropertyType)) { //when we have non empty collection //we cyle iterate the parsing operations IList val = (IList) pInfo.GetValue(source,null); if(val !=null && val.Count>0) { parseType(val[0],ref xmloverrides,updgElement); } } } }
getAttributesForProperty
プライベート呼び出しは、属性オーバーライドの実際の挿入を行います。
private void getAttributesForProperty( PropertyInfo pInfo, ref XmlAttributeOverrides xmloverrides, Type objType, UpdategramElement updgElement) { // need this as we start from the assumption that in // the Before we need the minimum in the After we need the more bool ignore = false; if(updgElement==UpdategramElement.Before) { ignore = true; } // Iterate through all the Attributes for each property. foreach (Attribute attr in Attribute.GetCustomAttributes(pInfo)) { Type tmp = attr.GetType(); //remove from before the lookup values //and leave the primary/foreign keys if(updgElement==UpdategramElement.Before) { if(tmp==typeof(IntegrityCheckAttribute)) { ignore = false; } if(tmp==typeof(LookupAttribute)) ignore = true; } //remove from after node //the lookup attribute (if it's not an insert) else if(updgElement==UpdategramElement.After) { if((tmp==typeof(LookupAttribute)) && this._Before!=null) ignore=true; } } if(ignore) { XmlAttributes myAttributes = new XmlAttributes(); myAttributes.XmlIgnore = true; xmloverrides.Add(objType,pInfo.Name,myAttributes); } }
コメントを読めば、コードのロジックは理解できるでしょう。次に示すのは、Order
オブジェクトに関する最終的なUPDATEGRAMです(先頭のOrderLine
が削除されています)。
<ROOT xmlns:updg="urn:schemas-microsoft-com:xml-updategram"> <updg:sync updg:nullvalue="|isnull|"> <updg:before> <Order updg:id="10250"> <OrderID>10250</OrderID> <OrderLines> <OrderDetail updg:id="41"> <OrderID>10250</OrderID> <ProductID>41</ProductID> </OrderDetail> <OrderDetail updg:id="51"> <OrderID>10250</OrderID> <ProductID>51</ProductID> </OrderDetail> <OrderDetail updg:id="65"> <OrderID>10250</OrderID> <ProductID>65</ProductID> </OrderDetail> </OrderLines> </Order> </updg:before> <updg:after> <Order updg:id="10250"> <OrderID>10250</OrderID> <OrderDate>1996-07-05T00:00:00.0000000+02:00</OrderDate> <RequiredDate>1996-08-07T00:00:00.0000000+02:00 </RequiredDate> <ShippedDate>1996-07-12T00:00:00.0000000+02:00 </ShippedDate> <Freight>65.83</Freight> <ShipName>Hanari Carnes</ShipName> <ShipAddress>Rua do Paulo, 69</ShipAddress> <ShipCity>Rio de Janeiro</ShipCity> <ShipPostalCode>283458</ShipPostalCode> <ShipRegion>RJ</ShipRegion> <ShipCountry>test</ShipCountry> <OrderLines> <OrderDetail updg:id="51"> <Quantity>35</Quantity> <Discount>0.15</Discount> <OrderID>10250</OrderID> <ProductID>51</ProductID> </OrderDetail> <OrderDetail updg:id="65"> <Quantity>15</Quantity> <Discount>0.15</Discount> <OrderID>10250</OrderID> <ProductID>65</ProductID> </OrderDetail> </OrderLines> </Order> </updg:after> </updg:sync> </ROOT>
まとめ
このプロトタイプは、多くの点で改善の余地があります。2つのオブジェクトの状態を適切に比較することでXMLを適切にクリーンアップしたり、Before
ノードとAfter
ノードの全プロパティを使用することで、オフライン環境でのデータ整合性を維持するためのデータセット内部DiffGramのような手段を実現したり、タイムスタンプを使用してこれを実現したりすることもできるでしょう。すべては、自分自身と自分が実現する実装にかかっています。ここで定義したのは、SQLXMLと.NET Frameworkを使用して、SQL Server上でのエンティティレベルのデータベース読み取り/書き込み操作を自動化するための基本的な枠組みにすぎません。これをどう活用するかは皆さん次第です。