SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

SQLXMLとシリアル化を使用して.NETオブジェクトをSQL Serverに保存する

UPDATEGRAMとXMLによるSQL Serverのデータ操作

  • X ポスト
  • このエントリーをはてなブックマークに追加

問題点

 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
      {
      }
   }

 OrderOrderDetailの属性を使用した新しい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上でのエンティティレベルのデータベース読み取り/書き込み操作を自動化するための基本的な枠組みにすぎません。これをどう活用するかは皆さん次第です。

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Gianluca Nuzzo(Gianluca Nuzzo)

MCAD認定上級Webデベロッパー。Microsoft製品とXMLを使ったWebアプリケーションに関して、長年にわたる開発経験を持つ。メールアドレスはgianluca_nuzzo@aliceposta.it

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/213 2006/02/10 11:29

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング