SQLXMLとシリアル化を使用して.NETオブジェクトをSQL Serverに保存するこの記事のURLhttp://japan.internet.com/developer/20060110/25.html
著者:Gianluca Nuzzo
海外internet.com発の記事
はじめに前の記事『SQLXMLとシリアル化を利用してSQL Serverからオブジェクトを取得する』を発表した後、私は、同記事で設計したスキーマ定義ファイルと、SQLXMLに含まれるUPDATEGRAMという別の技法を使用して、オブジェクトをデータベースに保存し直す方法について調べました。 この記事では、マイクロソフトが設計したこの技法について説明します。そして、シリアル化とカスタムクラス属性を使用し、SQL Serverに送信して自動生成文を実行できる適切なXMLストリームを形成する、 UPDATEGRAM機能まず最初に、Microsoft SQLXMLのUPDATEGRAM機能の概要を紹介します。UPDATEGRAM機能の詳細は、オンラインヘルプ(マシンにSQLXMLライブラリがインストールされている場合)またはMSDNサイトで見ることができます。この概念を要約すると、UPDATEGRAMは、XMLドキュメントであると共にXMLスキーマ定義であり、SQLXMLライブラリによって使用され、レコードを更新、挿入、および削除するコマンドの作成と実行をSQL Serverにおいて自動化するものと言えます。 各UPDATEGRAMは、主に、一連の データ更新用フォームの追加 さて、前の記事のWindowsアプリケーションに戻り、指定の グリッドフォームからフォームを開くためのDoubleClickイベントハンドラを追加し、次のコードを追加します。 private void grd_orders_DoubleClick(object sender, EventArgs e) { Form f = new OrderForm( this.orders[this.grd_orders.CurrentRowIndex]); f.Visible=true; } Loadイベントの中で、 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]ボタンの実装 まずに、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); } } 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へとシリアル化して ここで、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(); } private void InitUpdgDocument() { if(this.updgDocument==null) { //create empty updategram updgDocument = new XmlDocument(); updgDocument .LoadXml("<ROOT xmlns:"+prefix+"=""+ns+""></ROOT>"); } } 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(); } 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; } 既定のシリアル化では、すべての <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> 問題点SQLXMLに送信されると、次のエラーが発生します。 ![]() つまり、ノード(オブジェクト)ごとに、インデックスまたはIDを設定して、 [XmlAttribute(AttributeName="id", Namespace="urn:schemas-microsoft-com:xml-updategram")] public int OrderIDKey { get { return this.ProductID; } set { } } このケースの [XmlAttribute(AttributeName="id", Namespace="urn:schemas-microsoft-com:xml-updategram")] public int OrderIDKey { get { return OrderID; } set { } } <Order updg:id="10250"> <OrderLines> <OrderDetail updg:id="41"> このケースでは、SQL Serverに送信されても何も問題は生じません。 しかし、IT環境では、当初とまったく変わらないものなどあり得ません。そこで、既定のシリアル化が動作しないいくつかのケースと、その問題の解決方法について考えてみましょう。 null値の処理 まず、 SQLXMLのNULL処理については、MSDNライブラリで説明されています。XMLルート要素で定義される既定値を使用すると(例では [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; } } } フォームの
if(this.txtShipZip.Text==string.Empty)
this.order.nullProperty("ShipPostalCode");
if(this.txtShipCountry.Text==string.Empty)
this.order.nullProperty("ShipCountry");
UPDATEGRAMクラスの 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(); } ![]() リレーショナルデータの整合性とルックアップ値 もう1つの問題は、リレーショナルデータの整合性とルックアップ値に関するものです。次のコード行で、
this.order.OrderLines.RemoveAt(0);
AFTERノード内のこの後のXMLシリアル化が、削除された ![]() シリアル化のカスタマイズそこで、まず最初に、XMLシリアル化から除外するプロパティをマークする属性を作成しました。 public class LookupAttribute:System.Attribute { public LookupAttribute() {} } 次に、常に確実に必要なプロパティ(プライマリキーや外部キーなど)について考慮して、2番目の属性を作成しました。 public class IntegrityCheckAttribute:System.Attribute { public IntegrityCheckAttribute() {} } その後、プロパティを
[Lookup]
public Product Item
また、すべてのプライマリキーと外部キーを、 [IntegrityCheck] public int OrderID オブジェクトの状態に応じてマーク付きプロパティをXMLシリアル化から除外する、という機能を持ったオブジェクト用のカスタムシリアライザを作成できます。 このシリアライザの構成は次のとおりです。 ![]() コンストラクタのコードを次に示します。 public UpdgXSerializer(object objBefore,object objAfter) { //hold reference to before state this._Before=objBefore; //hold reference to after state this._After=objAfter; } UDATEGRAMノードの 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; } 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); } } } } 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); } } コメントを読めば、コードのロジックは理解できるでしょう。次に示すのは、 <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を適切にクリーンアップしたり、 著者紹介Gianluca Nuzzo(Gianluca Nuzzo)
MCAD認定上級Webデベロッパー。Microsoft製品とXMLを使ったWebアプリケーションに関して、長年にわたる開発経験を持つ。メールアドレスはgianluca_nuzzo@aliceposta.it。
japan.internet.comのウエブサイトの内容は全て、国際法、日本国内法の定める著作権法並びに商標法の規定によって保護されており、その知的財産権、著作権、商標の所有者はインターネットコム株式会社、インターネットコム株式会社の関連会社または第三者にあたる権利者となっています。
本サイトの全てのコンテンツ、テキスト、グラフィック、写真、表、グラフ、音声、動画などに関して、その一部または全部を、japan.internet.comの許諾なしに、変更、複製、再出版、アップロード、掲示、転送、配布、さらには、社内LAN、メーリングリストなどにおいて共有することはできません。 ただし、コンテンツの著作権又は所有権情報を変更あるいは削除せず、利用者自身の個人的かつ非商業的な利用目的に限ってのみ、本サイトのコンテンツをプリント、ダウンロードすることは認められています。 |