japan.internet.com
japan.internet.com メンバーID
Twitter
Facebook
RSS
ピックアップ
2007年2月6日 10:00

.NET データ処理に役立つ26のヒント(後編)

著者Kevin S. Goffオリジナル版を読む海外海外発

はじめに

 使用する言語がC#にせよVB.NETにせよ、また、アプリケーションのフロントエンドとバックエンドのどちらを主に担当するにせよ、データ処理の技能は、プロジェクトの中で自分がどれだけ力となれるかを直接左右する要因です。

 .NETの新しいジェネリッククラスでは、シンプルなコードで多くの処理を実行できるため、効率が飛躍的に向上します。また、ASP.NET 2.0のObjectDataSourceクラスでは、中間層のデータ処理クラスとWebページ上のデータバインドコントロールを簡単に連係できます。さらに、前編でも取り上げたように、T-SQL 2005には、データベース開発者の生産性向上に寄与する新しい言語機能が備わっています(今回の記事にも登場します)。

 データ処理をテーマとした全2回の後編となる今回は、.NETのジェネリック、ASP.NET 2.0のObjectDataSource、前回取り上げた以外のT-SQL 2005の新機能について解説します。

過去の記事

目的:データ処理の包括的なヒント集

 私は、太陽がさんさんと降り注ぐ自宅のテラスでこの文章を書いています。ちょうど、コミュニティイベントやカンファレンスでの講演回りの長旅を終えたばかりです。その経験で得た大量のエネルギーをこの記事に注入し、これまでになくホットなVisual Studio 2005とT-SQL 2005のデータ処理機能について解説したいと思います。

 では、プレイボールといきましょう。「The Baker’s Dozen」のダブルヘッダー第2戦、今回の打順を紹介します(もちろん、野球のメンバーは9人ですが、この記事のヒントは13個です)。

  • 1番〜7番は、.NETのジェネリックを活かすための7つのヒントです。今回の記事で特にポイントとなるのはヒント7です。型指定されたデータセットにストアドプロシージャからデータを読み込む、ジェネリックデータアクセスクラスについて取り上げます。
  • 中位打線のヒント8では、ASP.NET 2.0の新しいObjectDataSourceの機能について、簡単な例で紹介します。
  • 残る下位打線は、バックエンドのデータベースについてです。つまり、ヒント9〜13では、T-SQL 2005でのデータの処理方法について、前編で取り上げた以外の話題を解説します。

ヒント1:.NETのジェネリックの概要

 多くの場合、.NETのジェネリックは、Visual Studio .NET 2003で開発者が目指した処理に照らしてみるとわかりやすくなります。

 Visual Studio 2003では、オブジェクトをリストに格納するときに、ArrayListコレクションクラスを使用することがよくありました。次のコードのような形です。この例では、整数のリストを格納しているだけですが、フォームオブジェクトやデータテーブルなど、その他のオブジェクトのコレクションの場合も、同じように簡単に格納できます。

// Visual Studio 2003 code
ArrayList list = new ArrayList();
// implicit boxing, must convert value 
// type to reference type
list.Add(3);
list.Add(4);
list.Add(5.0);
int nFirst = (int)list[0];
int total = 0;
foreach (int val in list) 
    total = total + val;

 このコードには、次のような3つの問題が潜んでいます。

  • 1つ目は、ArrayListに実際に格納する具体的な型に関係なく、ArrayListにはオブジェクトとして格納されてしまうという点です。したがって、「ボックス化」という暗黙的な変換処理が発生します。
  • 2つ目は、ArrayListから特定のアイテムを取得するときに、.NETがそのアイテムをオブジェクトから元のデータ型に変換する必要があり、そのデータ型をプログラマが指定する必要があるという点です。したがって、「ボックス化解除」の処理が発生します。
  • 3つ目は、コンパイル時のタイプセーフ性のチェックがないという点です。上のコードでは、整数2つと実数1つをArrayListに格納しています。最後の2行で、各値に対して反復処理を行うときに、実行時エラーが発生します。実数値(5.0)を整数値にキャストしようとするからです。

 Visual Studio 2005では、System.Collection.Generic名前空間にある、新しいListクラスを使用できます。Listクラスでは、ArrayListクラスが持つ問題に対処できます。クラスのインスタンスに格納する型を具体的に指定できるからです。

// Visual Studio 2005 code
// define the stored data type in the placeholder 
List<int> aList = new List<int>();
aList.Add(3);
aList.Add(4);
// The following line generates a compile error
aList.Add(5.0);   
// no need for unboxing, value type stored in 
// List<int> as an int, not an object
int nFirst = aList[0];   
int total = 0;
foreach (int val in aList) 
    total = total + val;

 Listクラスのインスタンスを作成するときに、格納する型を指定しているので、.NETがボックス化やボックス化解除を実行する必要がなくなります。整数を格納するということを判断できるからです。また、List内の特定のアイテムをキャストする必要もありません。

 インスタンス化の回数がよほど多くない限り、パフォーマンスに差は出ないでしょうが、格納の効率やコンパイル時のタイプセーフ性が高まるという点だけでも、ArrayListクラスとListクラスの違いとしては十分すぎるほどです。

 とはいえ、ArrayListクラスの方がパフォーマンスが若干低いという点も、きちんと押さえておくことにしましょう。次のコードでは、ArrayListを作成し、100万個の整数を格納したうえで、リスト内の各項目に対して反復処理を実行します。このコードの実行には、約0.2秒を要します。

// Visual Studio 2003 code - takes .2 seconds
ArrayList aBigList = new ArrayList();
DateTime dt1, dt2;
dt1 = DateTime.Now;
for (int nCtr = 0; nCtr < 1000000; nCtr++)
    aBigList.Add(nCtr);
int nSum = 0;
foreach (int nn in aBigList)
    nSum += nn;
dt2 = DateTime.Now;
TimeSpan ts = dt2.Subtract(dt1);
MessageBox.Show(ts.TotalSeconds.ToString());

 一方、新しいListクラスを使う形でこのコードを書き直すと、実行に要する時間は0.04秒になります。つまり実行速度は5倍になります。

// Visual Studio 2005 code - takes .04 seconds 
List<INT> aBigList = new List<INT>();
DateTime dt1, dt2;
dt1 = DateTime.Now;
for (int nCtr = 0; nCtr < 1000000; nCtr++)
    aBigList.Add(nCtr);
int nSum = 0;
foreach (int nn in aBigList)
    nSum += nn;
dt2 = DateTime.Now;
TimeSpan ts = dt2.Subtract(dt1);
MessageBox.Show(ts.TotalSeconds.ToString());

 次に、ジェネリックメソッドを見てみましょう。これは非常に柔軟なコードを実現できる機能です。昨年のMSDN CodeCampのイベントでCarl Franklin(.NET Rocks!で有名)が話していたことですが、ジェネリックの優れた利用法の1つに、型のみが異なる複数のクラスでの使用があります。

 ここでは、2つの値を比較して、大きい方の値を返すというコードで考えてみることにしましょう。このコードは、文字列、日付、整数など、さまざまな値に対して使えるものとします。この場合、複数のメソッドを作成する方法、1つのメソッドをさまざまにオーバーロードさせる方法、何らかの裏技を使う方法などが考えられます。しかし、.NETのジェネリックを利用すれば、メソッドを1つ作成するだけで複数のデータ型に対応できるのです。これこそ、.NETのジェネリックの本領発揮です。

 次のコードでは、CalcMaxというメソッドを作成しています。Tという文字と、プレースホルダ<T>を使用している点が目を引きます。ここで使う文字はTでなくてもかまいません。別の文字や、まったく異なる単語を使用することもできます。しかし、型(Type)を表すプレースホルダとして、Tという文字は理にかなっています。

public T CalcMax<T> ( T compVal1, T compVal2)
       where T : IComparable 
{
    T returnValue = compVal2;
    if (compVal2.CompareTo(compVal1) < 0) 
        returnValue = compVal1;
    return returnValue;
}

 このCalcMaxメソッドでは、1つの戻り値と2つのパラメータに型のプレースホルダを定義しています。渡す型が従うべき唯一の規則は、IComparableを実装している必要があるという点です。なぜなら、パラメータのCompareToメソッドを使用するからです。

 CalcMaxメソッドは何回でも呼び出すことができ、そのたびに異なるデータ型を指定できます。

double dMax = CalcMax<DOUBLE>(111.11,333.23);
int intMax =  CalcMax<INT>(2, 3);
string cMax = CalcMax<STRING>(&quot;Kevin&quot;, &quot;Steven&quot;);
DateTime dtMax = CalcMax<DATETIME> 
   (DateTime.Today, DateTime.Today.AddDays(1));

ヒント2:ジェネリックによるカスタムクラスの格納

 Listクラスはカスタムクラスの格納にも使用できます。次のコードはBaseballClassという簡単なカスタムクラスです。チーム名と選手名を表すプロパティを持ちます。

using System;
using System.Collections.Generic;
using System.Text;
namespace GenericsDemo{
    public class BaseballClass  
    {
        private string name;
        public string Name
        {  get  { return name;  }}
        private string team;
        public string Team
        { get { return team; } }
        public BaseballClass(string name, string team)
        {
            this.name = name;
            this.team = team;
        }
        public override string ToString()
        {
            return team + ", " + name;
        }
    }
}

 次のコードに示す関数(PopulateBaseballClass)では、Listクラスのインスタンスを作成し、そこにBaseballClassのインスタンスを格納しています。

List<BaseballClass> PopulateBaseballClass()
{
    List<BaseballClass> oBaseball = new List<BaseballClass>();
    oBaseball.Add(new BaseballClass("Kenny Rogers", "Tigers"));
    oBaseball.Add(new BaseballClass("Michael Young", "Rangers"));
    oBaseball.Add(new BaseballClass("Ken Griffey, Jr.", "Reds"));
    oBaseball.Add(new BaseballClass("Tom Glavine", "Mets"));
    oBaseball.Add(new BaseballClass("David Ortiz", "Red Sox"));
    oBaseball.Add(new BaseballClass("Derek Jeter", "Yankees"));
    oBaseball.Add(new BaseballClass("Roger Clemens", "Astros"));
    oBaseball.Add(new BaseballClass("Roy Oswalt", "Astros"));
    return oBaseball;
}

 そして次のコードでは、BaseballClassのインスタンスを作成し、上記のPopulateBaseballClass関数を呼び出し、その後でこのコレクションを反復処理します。

private void TestBaseballClass()
{
    List<BaseballClass> oBaseball = new List<BaseballClass>();
    oBaseball = this.PopulateBaseballClass();
    string cResults = "";
    foreach (BaseballClass oRecord in oBaseball)
        cResults += oRecord + "
";
    MessageBox.Show(cResults);
}

ヒント3:.NETのジェネリックによる並べ替え

 Listジェネリッククラスには、並べ替え処理と検索処理を行うためのメソッドがあります。また、独自の並べ替えや検索のロジックを実装することもできます。それぞれ見てみることにしましょう。

 次のコードは、選手名による並べ替えと、チームごとの選手名による並べ替えの例です。いずれも、「匿名メソッド」というC#の新しい言語機能を使用して、並べ替えのロジックをインラインで指定しています。

private void TestSortBaseballClass()
{
    List<BaseballClass> oBaseball = new List<BaseballClass>();
    oBaseball = this.PopulateBaseballClass();
    string cResults = "";
   
    // Anonymous delegate to sort on name
    oBaseball.Sort (delegate(BaseballClass f1, BaseballClass f2)
    {
        return f1.Name.CompareTo(f2.Name);
    }
   
    // Anonymous delegate to sort on Team, then Name
    oBaseball.Sort (delegate(BaseballClass f1, BaseballClass f2)
    {
        return f1.Name.Insert(0, f1.Team).CompareTo(
                                       f2.Name.Insert(0, f2.Team));
    }
}

 匿名メソッドを使用すると、コードブロックをデリゲートにカプセル化でき、通常ならメソッド名を指定する部分に、インラインでコードを記述できます。メソッドを別途作成する必要がないため、デリゲートをインスタンス化するときのコーディングの手間を減らすことができます。デリゲートの代わりにコードブロックを指定する方法は、メソッドを作成するのが無駄な手間に感じられる場合に有効です。

 匿名メソッドの構文は、最初は扱いづらいように思えるかもしれません。しかし次の例では、匿名メソッドを使うことによって、クラスを余分に追加する手間が省けます。

// In-line anonymous method that compares 2 
// incoming values
oBaseball.Sort(delegate(BaseballClass f1, BaseballClass f2) {
    return f1.Name.CompareTo(f2.Name);
});
   
// A second anonymous method, inserts team 
// in front of name, to sort on two columns 
oBaseball.Sort(delegate(BaseballClass f1, BaseballClass f2) {
    return f1.Name.Insert(0, f1.Team).CompareTo( 
                                        f2.Name.Insert(0, f2.Team));
});

ヒント4:.NETのジェネリックでの検索と匿名メソッド(オプション)

 .NETのListジェネリッククラスには、検索を実行するためのメソッドとして、FindFindAllFindLastがあります。次の例のように、独自の検索ロジックをインラインで実装する匿名メソッドを作成することもできますし、Find系メソッドのPredicate(ブール値関数)となるクラスを作成することもできます(リスト1とリスト2を参照)。

private void TestFindBaseballClass()
{
    List<BaseballClass> oBaseball = new List<BaseballClass>();
    oBaseball = this.PopulateBaseballClass();
    string cSearch = "Astros";
    BaseballClass oSingleFind =
        oBaseball.Find( delegate(BaseballClass f1) 
        {
            return f1.Team == cSearch;
        });
    MessageBox.Show("Single find is " + oSingleFind);
    string cResults = "";
    foreach (BaseballClass oRecord in
        oBaseball.FindAll( (delegate(BaseballClass f1) 
        {
            return f1.Team == cSearch;
        })))
        cResults += oRecord + "
";
    MessageBox.Show("Multiple find is " + oSingleFind);
}

 上のコードでは、その前の例と同じ手法を使っています。つまり、oBaseballの中身を検索して条件に一致する最初のインスタンスを探すデリゲートをインラインメソッドで作成しています。仮にここで、完全一致ではなく部分文字列による検索を実装するとしたら、匿名メソッド内で別の文字列関数を使用することも可能です。

 ListクラスのFindAllメソッドについても、同様の方法で使用できます。インラインのデリゲートと匿名メソッドを指定し、受け取った各BaseballClassオブジェクトで検索文字列に一致するものをブール値で返しています。FindAllメソッドでは、ADO.NETのDataTableオブジェクトのようなフィルタ機能を実装できます。

string cSearch = "Astros";
   
// Perform a single search on the first hit
BaseballClass oSingleFind = oBaseball.Find(
    delegate(BaseballClass f1)
    { 
        return f1.Team == cSearch;
     });
   
// Perform a find all (almost like a filter)
string cResults = "";
foreach (BaseballClass oRecord in   
    oBaseball.FindAll((delegate(BaseballClass f1)
    { 
        return f1.Team == cSearch;
    })))
    cResults += oRecord + "
";

 独立したクラスとして作成したい場合(あるいはVisual Studio 2005で匿名メソッドをサポートしていないVB.NETを使用している場合)ために、独立したFindクラスの例とその使用例をリスト1とリスト2に示しておきます。

リスト1 ブール型メソッドを含む検索クラス
using System;
using System.Collections.Generic;
using System.Text;
namespace GenericsDemo
{
    class FindBaseballClass
    {
        private string team;
        public FindBaseballClass(string team)
        {
            this.team = team;
        }
        // Implement specific search logic - return true or false
        public bool TeamFind(BaseballClass oBaseball)
        {
            return oBaseball.Team == team;
        }
    }
}
リスト2 検索クラスの使用例(匿名メソッドなし)
private void TestFindBaseballClass()
{
   List<BaseballClass> oBaseball = new List<BaseballClass>();
   oBaseball = this.PopulateBaseballClass();
   FindBaseballClass oFind = new FindBaseballClass("Astros"); 
   BaseballClass oSingleFind = oBaseball.Find(  
      new Predicate<BaseballClass>(oFind.TeamFind));
   MessageBox.Show("Single find is " + oSingleFind);
   string cResults = "";
   foreach (BaseballClass oRecord in oBaseball.FindAll(
      new Predicate<BaseballClass>(oFind.TeamFind)))
      cResults += oRecord + "
";
   MessageBox.Show("Multiple find is " + oSingleFind);
}

ヒント5:Listクラスへの他オブジェクトの格納

 前述のとおり、新しいListクラスには、.NETのどんな型でも格納できます。リスト3は、特定のインターフェイスを実装するクラスを格納する例です。たとえば、複数のレポートクラスがあり、それらはすべてGenerateDocumentというメソッドを持つとします。次のような処理を行うことで、メインの制御ループ構造を作成できます。

  • GenerateDocumentという1つのメソッドを持つ、IDocumentというインターフェイスを定義します。
  • IDocumentを実装する複数のレポートドキュメントクラス(SimpleClass1SimpleClass2など)を作成します。
  • IDocumentインターフェイスを実装しているさまざまなクラスを格納するためのListオブジェクトのインスタンスを作成します。
  • List<SimpleInterface.IDocument> oList = 
        new List<SimpleInterface.IDocument>();
    
  • レポートドキュメントクラス(SimpleClass1など)のインスタンスを作成し、リストにインスタンスを追加します。
  • リストを反復処理し、GenerateDocumentメソッドを呼び出します。
  • foreach(SimpleInterface.IDocument oClass in oList)
        oClass.GenerateDocument();
    

 この例については、GE HealthcareのRob Haleに感謝します。Robは、Boston CodeCampで私が開催したC# 2.0セッションの参加者で、このコードのアイデアの発案者です。本当にありがとう、Rob!

リスト3 インターフェイスを実装するオブジェクトのコレクション
public class SimpleInterface
{
   public interface IDocument
   {
      void GenerateDocument ();
   }
}
class SimpleClass1 : SimpleInterface.IDocument
{
   public SimpleClass1() 
   { 
   }
   public void GenerateDocument()
   {
      // Method code goes here!
   }
}
class SimpleClass2 : SimpleInterface.IDocument
{
   public SimpleClass2() 
   { 
   }
   public void GenerateDocument()
   {
      // Method code goes here!
   }
}
List<SimpleInterface.IDocument> oList = 
   new List<SimpleInterface.IDocument>();
SimpleClass1 oClass1 = new SimpleClass1();
SimpleClass2 oClass2 = new SimpleClass2();
oList.Add(oClass1);
oList.Add(oClass2);
foreach (SimpleInterface.IDocument oClass in oList)
   oClass.GenerateDocument();

ヒント6:Generics名前空間のその他のクラス

 ここまでは、Visual Studio 2005の新しいListクラスについて説明してきました。ここでは、System.Collections.Generics名前空間で目を引くその他の3つのクラス、DictionarySortedDictionaryCollectionについて説明します。

 Visual Studio 2005には、Dictionaryという新しいクラスが追加されています。この抽象クラスを使うと、コレクションのキーと要素を対応付けることができます。.NET Framework 2.0の関数の中には、Dictioanryクラスを使用しているものもあります。その一例が、TCPチャネルを新規作成するための新しいオーバーロードです。次の例では、新しいDictionaryオブジェクトを作成する際に、キーと値の両方が文字列であることを指定しています。

// Create a new dictionary object with two strings
Dictionary<string, string> oDict = new Dictionary<string, string>();
oDict.Add("secure", "true");
oDict.Add("password", "kevin");
TcpChannel oChannel = new TcpChannel(oDict, null, null);

 ディクショナリのエントリをキーによって並べ替える必要がある場合は、キーによる並べ替え順序が維持される、新しいSortedDictionaryクラスを使用するのがよいでしょう。

 また、ここまでは、新しいListクラスを使用してきましたが、Visual Studio 2005には、軽量なCollectionクラスもあり、並べ替え機能やフィルタ機能が必要ない単純なリストの場合にはそちらを使用できます。

Collection<int> oCol = new Collection<int>();
oCol.Add(2);
oCol.Add(3);
oCol.Insert(0,1);

 Collectionオブジェクトにはカスタムクラスも格納できます。

Collection<BaseballClass> oBaseballCollection = 
    new Collection<BaseballClass>();
oBaseballCollection.Add(new BaseballClass("Tom Glavine",
                                          "Mets"));
oBaseballCollection.Add(new BaseballClass("Derek Jeter",
                                          "Yankees"));
foreach (BaseballClass oBaseball in 
    oBaseballCollection)
MessageBox.Show(oBaseball.ToString());

 また、.NETのジェネリックには、リンクリストデータを扱うためのLinkedListクラスやLinkedListNoteクラスなどのほか、Queueクラス、Stackクラスもあります。

ヒント7:.NETのジェネリックを使用して型指定されたデータセットにレコードを読み込むデータアクセスクラスの作成

 私はストアドプロシージャと型指定されたデータセットを多用します。私にとって、大いなる目標の1つだったのが、型指定されたデータセットに対して、ストアドプロシージャから直接レコードを読み込むということでした。

 長いこと私が用いてきたのは、データアクセスクラスのベースメソッドを使用して、パラメータを持つストアドプロシージャから普通のデータセットにレコードを読み込むという方法でした。そのデータセットを、型指定されたデータセットに後でマージします。その方法でも確かに動作しますが、余分なコードと余分な処理が必要です。私がしたかったのは、型指定されたデータセットのインスタンスをベースメソッドに渡すと、そのベースメソッドがファクトリの役割を果たし、中身がセットされたデータセットが返ってくる、という方法でした。

 .NETのジェネリックを利用すると、そのようなクラスを作成し、使用することができます(リスト4とリスト5を参照)。そのようなクラスを作成するには、次の手順に従います。

  • 型指定されたデータセットのインスタンスと、データアクセスクラスのインスタンスを作成します(リスト5を参照)。
  • 型指定されたListを作成し、ストアドプロシージャで使用するSQLパラメータを格納します(ArrayListは使用しません)。
  • データアクセスクラスのメソッド(ReadIntoDs)を呼び出し、型指定されたデータセットのインスタンス、ストアドプロシージャの名前、ストアドプロシージャのパラメータが格納された型指定Listを渡します。
  • データアクセスメソッドReadIntoDsの本体を作成します(リスト5を参照)。第1パラメータと戻り値には、型指定されたプレースホルダを指定します。このパラメータにはデータセットのみを使用できるように指定しています。メソッド内のコードでデータセット固有のプロパティやメソッドを使用するからです。
  • public T ReadIntoDS<T>
          (T dsTypedDs, string cStoredProc, 
          List<SqlParameter> oParmList) 
       where T : DataSet
    
  • 標準のSqlConnectionオブジェクトやSqlDataAdapterなどを定義します。
  • この部分は力技です。SQL Serverから返るストアドプロシージャの結果セットでは、Table1、Table2、といった名前が使われています。一方、型指定されたデータセットを自分でデザインするときには、もっとわかりやすい名前(dtClientdtDetailsなど)を使用する可能性があります。したがって、Table、Table1といった名前を、型指定されたデータセットの名前に対応付ける必要があります。それには、DataAdapterTableMappingsを使用します。
  • DataAdapterからデータセットにレコードを読み込み、返します。
リスト4 ジェネリックデータアクセスメソッド
public class SimpleDataAccess
{
    // Pass the following:
    // 1) An instance of a typed dataset you wish to fill
    // 2) The name of the stored procedure
    // 3) A List of SqlParameters 
    // Function will execute the stored procedure,  
    // fill the typed dataset with the results, and then return it
    public T ReadIntoDs<T> 
            (T oTypedDs, string cSP, List<SqlParameter> oParms) 
        where T:DataSet
    {
        SqlConnection oSqlConn = this.GetConnection();
        SqlDataAdapter oSqlAdapt = new SqlDataAdapter(cSP, oSqlConn);
        oSqlAdapt.SelectCommand.CommandType = 
        CommandType.StoredProcedure;
        foreach (SqlParameter oParm in oParmList)
            oSqlAdapter.SelectCommand.Parameters.Add(oParm);

        // A little bit of "elbow grease…
        // SQL Server returns tablenames as "Table", "Table1", etc
        // We need to map in the tablenames from the typed DS
        // So loop through the tables in the Typed DS, get the name,
        // and map it to the corresponding table that SQL will
        // use as the default name
        int nTableCtr = 0;
        foreach (DataTable Dt in dsTypedDs.Tables) {
            string cSource = "";   
            if (nTableCtr == 0)
                cSource = "Table";
            else
                cSource = "Table" + nTableCtr.ToString().Trim();
        oSqlAdapter.TableMappings.Add(
            cSource, Dt.TableName.ToString());
        nTableCtr++;
    }
        // End of "elbow grease" - we can fill the dataset
        // from the adapter, now that we’ve mapped our tablenames
        oSqlAdapter.Fill(dsTypedDs);
        return dsTypedDs;
    }

    public SqlConnection GetConnection() 
    {
        // New SqlConnection StringBuilder class
        SqlConnectionStringBuilder oStringBuilder = 
            new SqlConnectionStringBuilder();
        oStringBuilder.UserID = "UserID";
        oStringBuilder.Password = "PassWord";
        oStringBuilder.InitialCatalog = "InitialCatalog";
        oStringBuilder.DataSource = "DataSource";
        return new SqlConnection(oStringBuilder.ConnectionString);
    }
}
リスト5 リスト4のジェネリックデータアクセスメソッドの使用例
// Create an instance of a typed DataSet
dsAgingReport odsAgingReport = new dsAgingReport();

// Create an instance of our Data Access Method
SimpleDataAccess oDataAccess = new SimpleDataAccess();

// Use the list class to create a collection of sql parameters
List<SqlParameter> oParms = new List<SqlParameter>();
oParms.Add(new SqlParameter("@dAgingDate", DateTime.Today));
oParms.Add(new SqlParameter("@lShowDetails", true));

// Call ReadIntoDs, passing the typed DS, name of the stored proc
// and the list of parameters
odsAgingReport = oDataAccess.ReadIntoDs(odsAgingReport,
   "[dbo].[GetAgingReceivables]",oParms);

ヒント8:ASP.NET 2.0のObjectDataSourceの概要

 本記事の前編で、ASP.NET 2.0のGridViewについて取り上げ、データセットにバインドする方法や、並べ替えとページングの処理方法について解説しました。もう1つ使用できるのが、ASP.NET 2.0のObjectDataSourceです。データを処理するビジネスオブジェクト/クラスを、GridViewなどのデータバインドコントロールに対して公開できます。

 ここでは、ObjectDataSourceの使用方法について、ステップバイステップ方式で解説します。最初の例では、「保留」になっている注文が格納された、型指定されたデータセットを返すビジネスオブジェクトを作成します。その後で、GridViewを配置したWebページを作成し、そのGridViewの直接のDataSourceとして、前述のビジネスオブジェクトを指定します。

  1. 「bzOrders」という別個のプロジェクトを作成します。型指定された単純なデータセットdsOrdersと、レコードが読み込まれたdsOrdersのインスタンスを返すメソッドGetOrdersOnHoldを作成します。
  2. 新しいWebページプロジェクトを作成し、「bzOrders.dll」を参照として追加します(図1を参照)。
  3. 図1 ビジネスオブジェクトへの参照を追加し、ObjectDataSourceで使用する
    図1 ビジネスオブジェクトへの参照を追加し、ObjectDataSourceで使用する
  4. 新しいWebページで、ツールボックスからObjectDataSourceのインスタンスを作成します(図2を参照)。
  5. 図2 ObjectDataSourceのインスタンスをツールボックスからドラッグし、Webページにドロップする
    図2 ObjectDataSourceのインスタンスをツールボックスからドラッグし、Webページにドロップする
  6. 新しいObjectDataSourceを右クリックし、データソースを設定するオプションを選択します。[Configure Data Source]ダイアログボックスが表示されます(図3を参照)。データを提供するクラスとしてbzObjectsを選択します。
  7. 図3 新しいObjectDataSourceを右クリックしてビジネスオブジェクトを選択する
    図3 新しいObjectDataSourceを右クリックしてビジネスオブジェクトを選択する
  8. 次に、標準のSelectInsertUpdateDeleteの各操作に使用するメソッドをObjectDataSourceに対して指定します。この例では、Selectコマンドのみを指定し、GetOrdersOnHoldメソッドとします(図4を参照)。
  9. 図4 データを取得するためのビジネスオブジェクトのメソッドを選択する
    図4 データを取得するためのビジネスオブジェクトのメソッドを選択する
  10. Webページ上にGridViewを作成し、DataSourceIDプロパティをObjectDataSourceに設定します(図5を参照)。また、GridViewのその他のプロパティを図5のように設定します。並べ替えとページングを有効にするオプションも設定します。
  11. 図5 GridViewを作成し、DataSourceIDやその他のプロパティを設定する
    図5 GridViewを作成し、DataSourceIDやその他のプロパティを設定する
  12. この時点で、Webページは実際に動作します。しかし列のサイズや配置の設定、見出しのカスタマイズなどを行っておく方がよいかもしれません。GridViewのプロパティシートを表示し、Columnsコレクションを選択します。列/フィールドのデザイナが表示され(図6を参照)、GridViewをカスタマイズできます。
  13. 図6 選択したフィールドが既に確立されており、表示をカスタマイズできる
    図6 選択したフィールドが既に確立されており、表示をカスタマイズできる
  14. これで完成です。Webページを実行すると、図7のような結果になります。
  15. 図7 最終的な結果。コードなしで完成!
    図7 最終的な結果。コードなしで完成!
注記
 重要な補足を1つ。図7を見るとわかるように、このページには、別のページに移動するためのリンクが表示されています。また、列見出しは、クリックして並べ替えを実行できるようになっています。ObjectDataSourceにバインドすると、並べ替えとページングが自動的に処理されます。コードを追加する必要はありません。

 このヒントで紹介したのは、ObjectDataSourceクラスの機能のほんの一部です。ObjectDataSourceでは、次のような処理も実現できます。

  • パラメータを使用して単一の注文を取得するretrieveメソッドをビジネスオブジェクトに定義できます。そして、ObjectDataSourceのデザイナで、そのメソッドをretrieveメソッドとして指定し、ページコントロールの値、FormFieldの値、QueryString、またはSession変数をパラメータに指定できます。
  • 標準的なInsertUpdateDeleteの各機能を実行するメソッドをビジネスオブジェクトに定義できます。こちらも、ObjectDataSourceのデザイナで、メソッドの対応付けとパラメータを指定できます。

 それからもう1つ、ObjectDataSourceが返したデータを「捕捉」するにはどうすればいいのか、疑問に思う人もいるかもしれません。たとえば、グリッドのキャプションに行数を表示したい場合などです。そのときは、ObjectDataSourceのSelectedイベントを使用します。

protected void ObjectDataSource1_Selected(
        object sender, ObjectDataSourceStatusEventArgs e)
{
    bzOrders.dsOrders odsOrders = 
        (dsOrders)e.ReturnValue;
    int nRows = odsOrders.dtOrders.Rows.Count;
    this.grdOrdersOnHold.Caption = 
        "Number of rows: " + nRows.ToString();
}

ヒント9:SQL ServerのCOALESCE関数の使用

 いくつかの検索条件(姓、名、住所、都市名、郵便番号など)をユーザーが入力すると、それに合致する顧客を検索できるというフォーム(またはWebページ)があるとしましょう。ユーザーが入力するのは、1つのフィールドでも複数のフィールドでもよいものとします。この場合、取り得るパラメータすべてをチェックしたうえで、ユーザーが入力したパラメータのみを使用してクエリするようなストアドプロシージャを作成する必要があります。

 この場合、各パラメータをチェックし、ユーザーが入力したパラメータに基づいてSQLのSELECT文字列を組み立て、動的SQLを使用してその文字列を実行する、という形でストアドプロシージャを作成する方法もあります。多くのSQL開発者が選択するのはそちらの方法です。

 ここでは別の方法を紹介します。次のコードのように、SQL ServerのCOALESCE関数を使用する方法です。

-- This will work in both SQL2000 and SQL2005
-- You can use COALESCE to query on only those search values
-- that are not NULL
DECLARE @city varchar(50), @state varchar(50), @zip varchar(50),
        @FirstName varchar(50), @LastName varchar(50), 
        @Address varchar(50)
SET @FirstName = ’Kevin’
SET @State = ’NY’
SELECT * FROM CUSTOMERS WHERE 
    FirstName = COALESCE(@FirstName,FirstName) AND
    LastName = COALESCE(@LastName,LastName) AND
    Address = COALESCE(@Address,Address) AND
    City = COALESCE(@City,City) AND
    State = COALESCE(@State,State) AND
    Zip = COALESCE(@Zip,Zip)

 COALESCEは、SQL Server 2000とSQL Server 2005の両方で使用でき、その方がT-SQLコードがすっきりすると思います。それぞれの検索条件について、COALESCEには2つの値を渡しています。1つは検索変数、もう1つは検索変数がNULLの場合に使用する値です。したがって、ユーザーが指定しなかった検索条件については、その列自身が既定値として使用されます。この方法は、クエリの対象行が数百万に及ぶ場合でも、きわめて高速に動作します。

ヒント10:SQL 2005でのグループ内のランク付け

 SQL Server 2005には、ROW_NUMBERという関数が追加されており、結果セットにランクを付けることができます。また、グループ分けした結果ごとにランクを付けることも可能です。たとえば、500ドルを超える上位の注文を対象として、顧客別に注文の多い順にランキングを付けたいとします。

 リスト6は、NorthwindのOrdersデータベースに対して、500ドルを超える注文をクエリする方法を示します。ランキングの番号は、顧客別の注文ごとに割り当てます。ROW_NUMBER OVERステートメントでは、PARTITION(ここでは、グループレベルの定義のようなものを表す)と、結果セットにランクを付けるときに使用するOrderを指定できます。

ROW_NUMBER() OVER (PARTITION BY CUSTOMERID 
ORDER BY  (UnitPrice  * Quantity) DESC) 
AS OrderRank
リスト6 ROW_NUMBERとPARTITIONを使用してレベル内でランクを付ける
SELECT  CustomerID, OH.OrderID, OrderDate,  
    (UnitPrice  * Quantity) as Orderamount,
    ROW_NUMBER() OVER (PARTITION BY CUSTOMERID 
ORDER BY  (UnitPrice  * Quantity) desc ) AS OrderRank
FROM Orders OH
    JOIN [dbo].[Order Details] OD
ON OH.OrderID = OD.OrderID
WHERE  (UnitPrice * Quantity) > 500
ORDER BY CUSTOMERID, OrderAmount  DESC

-- Results, OrderRank partitioned (reset) on each Customer  
ALFKI   10692   1997-10-03 00:00:00.000   878.00   1
ALFKI   10835   1998-01-15 00:00:00.000   825.00   2
ANTON   10535   1997-05-13 00:00:00.000   1050.00   1
ANTON   10677   1997-09-22 00:00:00.000   936.90   2
ANTON   10535   1997-05-13 00:00:00.000   825.00   3
ANTON   10573   1997-06-19 00:00:00.000   820.00   4
ANTON   10573   1997-06-19 00:00:00.000   702.00   5
AROUT   10953   1998-03-16 00:00:00.000   4050.00   1
AROUT   10558   1997-06-04 00:00:00.000   1060.00   2
AROUT   10707   1997-10-16 00:00:00.000   780.00   3

ヒント11:T-SQL 2005のその他の話題(APPLY、ユーザー定義関数、相関サブクエリでのテーブル値ユーザー定義関数)

 T-SQL 2000では、テーブル値ユーザー定義関数をクエリに組み込む機能について、やや欠けている面がありました。開発者は、ユーザー定義関数の実行結果を使用するときに、まず一時テーブルに取り込んだうえで、その一時テーブルを使用するという方法を使わざるを得ないことがよくありました。

 T-SQL 2005では、テーブル値ユーザー定義関数とクエリの連係がしやすくなっています。この記事の前編では、新しいAPPLY演算子を使用して、テーブル値ユーザー定義関数の結果を派生テーブルと同じような形で直接適用するという例を紹介しました。その例では、指定した顧客のTOP Nの注文をNorthwindデータベースから取得するユーザー定義関数を使用し、クエリ内ですべての顧客に対してその関数を直接適用するという処理を行いました。

 今回は、テーブル値ユーザー定義関数をもっと直接的に使う別の方法を紹介します。T-SQL 2005では、テーブル値ユーザー定義関数をサブクエリで使用でき、外部クエリの列をユーザー定義関数の引数として渡せるのです。

 Northwindデータベースで、5,000ドルを超える注文が2件以上ある顧客や、1,000ドルを超える注文が5件以上ある顧客などを知りたいとします。最初の手順は、GetCustOrders_GT_Xというテーブル値ユーザー定義関数を作成することです(リスト7の前半部分を参照)。このユーザー定義関数は、2つのパラメータ(顧客IDと基準額)を受け取り、その顧客の注文で基準額を超えるものをテーブル変数で返します。

 次の手順では、データベースの各顧客に対してそのユーザー定義関数を実行し、注文が2件以上ある顧客を判断します。できれば、サブクエリを作成して、各顧客をパラメータとしてユーザー定義関数に渡せれば理想的です。ここで、T-SQL 2005の機能を活用できます。

 SQL Server 2000の場合、相関サブクエリ内のテーブル値関数では、外部クエリの列を参照できませんでした。うれしいことに、SQL Server 2005ではその制約がなくなりました。ユーザー定義関数を使用するサブクエリを作成して、外部クエリの列をユーザー定義関数に引数として渡すことができます(リスト7の後半部分を参照)。

リスト7 サブクエリ内でユーザー定義関数を使用する
CREATE FUNCTION [dbo].[GetCustOrders_GT_X]
   (@CustomerID AS varchar(10), @nThreshold AS decimal(14,2))
RETURNS @tOrders TABLE (OrderID int, CustomerID varchar(10), 
   OrderDate datetime, OrderAmount decimal(14,2))
AS
BEGIN
   INSERT INTO @tOrders
   SELECT  OH.OrderID, CustomerID, OrderDate,
      (UnitPrice  * Quantity) as Orderamount
   FROM Orders OH
   JOIN [dbo].[Order Details] OD
      ON OH.OrderID = OD.OrderID
   WHERE CustomerID =  @CustomerID AND 
             (UnitPrice  * Quantity)  > @nThreshold  
   ORDER BY OrderAmount  DESC
   RETURN
END
GO

DECLARE @nNumOrders int, @nMinAmt decimal(14,2)
SET @nNumOrders = 2
SET @nMinAmt = 5000.00
SELECT CustomerID FROM Customers 
WHERE  (SELECT COUNT(*) FROM 
     DBO.GetCustOrders_GT_X(CustomerID,@nMinAmt)) >=@nNumOrders

-- ResultsHUNGO
QUICK
ERNSH
SAVEA
RATTC

-- To get the actual orders > 5000 for these 5 customers, 
-- we can turn the query above into a derived table, and then 
-- use a CROSS APPLY
SELECT MaxOrders.* FROM
   (SELECT CustomerID FROM Customers
WHERE  (SELECT COUNT(*)  
FROM dbo.GetCustOrders_GT_X(CustomerID,@nThresholdAmt)) >= 
        @nNumOrders) as CustList
CROSS APPLY GetCustOrders_GT_X(CustomerID,@nThresholdAmt) 
AS MaxOrders

ヒント12:SQL 2005のXML機能の拡張

 SQL Server 2000には、XMLを処理するための多種多様な機能があります。SQL Server 2005では、新しいXMLデータ型によって、XML機能が拡張されています。

 リスト8とリスト9は、XMLデータを扱ういくつかの機能の例です。リスト8では、複数の異なる方法で、標準の列にXMLデータを挿入しています。リスト9では、XML列の中で文字列を検索しています(この構文については、Microsoft SQL Serverニュースグループの常連さんたちにご協力いただきました。ありがとうございました)。

リスト8 標準の列にXMLデータを挿入する
-- First Example of inserting XML into a table of columns
DECLARE @cXMLDoc XML
declare @hdoc int
SET @cXMLDoc = ’
<AddressType  >
   <AddressRecord AccountID = "1" 
      Street="31 Main Dr" City="Philly" State="PA" Zip="12345"/>
   <AddressRecord AccountID = "2" 
      Street="1 Wilson Dr" City="Newark" State="NJ" Zip="22222"/>
</AddressType>’

EXEC sp_xml_preparedocument @hdoc OUTPUT, @cXMLDoc
SELECT * FROM OPENXML (@hdoc, ’/AddressType/AddressRecord’,1)
WITH (AccountID int,Street varchar(100), City varchar(100), 
   State varchar(10), ZipCode varchar(13))

-- Second example of inserting XML into a table of columns
-- uses the Address tag to specify nested columns
DECLARE @cXMLDoc XML
declare @hdoc int
SET @doc = ’
<customer>
   <Customernum>48456</Customernum>
   <Firstname>Kevin</Firstname>
   <Lastname>Goff</Lastname>
   <Address>
      <city>Allentown</city>
      <state>PA</state>
   </Address>
</customer>’

EXEC sp_xml_preparedocument @idoc OUTPUT, @doc
DECLARE @tTemp TABLE (Customernum int, Firstname char(50), 
   Lastname char(50), City char(50), State Char(10))
INSERT INTO @ttemp
SELECT Customernum,firstname,lastname,city,state
FROM OPENXML (@idoc, ’/customer’,2)
WITH (Customernum int,Firstname varchar(50),Lastname varchar(50),
   city varchar(50) ’./Address/city’, 
   state varchar(50) ’./Address/state’)
リスト9 XMLデータ型内で検索を行う
-- Performing a partial text search inside an XML column
declare @tTest table (address xml)
insert into @ttest values (’
  <Address >
    <AddrRecord AccountID = "1" Street="31 Main Dr" City="Newark" 
       State="NJ" Zip="11111" />
  </Address>’ )

insert into @ttest values (’
  <Address >
    <AddrRecord AccountID = "2" Street="1 Wilson Rd" City="Philly" 
      State="PA" Zip="22222"/>
  </Address>’ )
SELECT * FROM @ttest WHERE
   Address.exist(’/Address/AddrRecord [contains(@City,"hil")]’) = 1

ヒント13:SQL 2005でのテーブル変数に関する制限の緩和

 テーブル変数は、SQL Server 2000で導入され、多くのデータベース開発者から歓迎されました。しかし、テーブル変数にはいくつかの制限がありました。その1つが、ストアドプロシージャの結果をテーブル変数に直接挿入できないという点でした。したがって、次のコードはT-SQL 2000では使えません。

INSERT @tTable EXEC <sp_mystoredproc> 

 うれしいことに、T-SQL 2005ではその制約がなくなりました。ストアドプロシージャを実行して、テーブル変数に直接挿入できます(リスト10を参照)。

リスト10 ストアドプロシージャの結果をテーブル変数に直接挿入する
DECLARE @tUser TABLE (UserName varchar(100),Status char(10),UserPK int)
INSERT INTO @tUser exec  [dbo].[cgsValidateUserID] ’KGOFF’,’KGOFF’
SELECT * FROM @tUser

データ処理についての雑感

 これまで何度も述べたことですが、結局のところ、開発者の仕事の主軸となるのはデータ処理です。この記事で2回にわたって見てきたように、Visual Studio 2005とSQL Server 2005では、データ処理が扱いやすくなっています。その中には、ジェネリックやSQLのPIVOTコマンドのように、大幅な機能拡張もあれば、ADO.NETでの個別の行ごとのフィルタのように、細かいながら重要な変更もあります。

 Visual Studio 2003のコードをVisual Studio 2005の環境でメンテナンスしている方なら、こうした新しいデータ処理機能をすぐには活用できないかもしれません。しかし、時間の許す限り、新機能について学び、その機能を使ったプロトタイプを作成して、いざというときのために備えておきましょう。

参考文献

 ばっちり空調の効いた部屋に1週間ほど閉じこもって、こうした新機能についてひたすら掘り下げてみたいと思ったことはありませんか。あいにく、現実世界にはさまざまな要件や要求があるため、その実現は通常は困難です。しかし、数時間の余裕があるのなら、次に挙げるような優れた参考文献で学ぶことが可能です。

 1つ目は、『CoDe Magazine 』誌の記事です。同誌には、SQL Server 2005でのT-SQLとXMLについて、すばらしい記事が掲載されています。2005年1月/2月号には、Jim Duffyの「SQL Server 2005 T-SQL Enhancements」という記事があります。Jimは話が上手で、テクノロジ全般に精通しています。

 『CoDe Magazine』誌の2006年5月/6月号には、Shawn Wildermuthによる「Making Sense of the XML DataType in SQL Server 2005」というすばらしい記事が掲載されています。もし「年間最優秀記事賞」のようなものがあったとしたら、私はこの記事に1票を投じたいと思います。Shawnの名前をGoogleで検索してみると、ADO.NETに関する有益な記事がネット上で多数見つかります。

 2つ目は、Manuel Abadiaがネット上で公開している、ObjectDataSourceに関する文章です。ExtendedObjectDataSourceという独自クラスも公開されています。Manuelのコンテンツはここから参照できます。

 最後に紹介するのは書籍です。Tod Goldingが書いた『Professional .NET 2.0 Generics』(Tod Golding 著、Wiley Publishing、2005年10月)はたいへん優れた本で、ジェネリックについて詳しく解説しています。コードはC#とVB.NETの両方で示されています。

最後に

 文章、書類、コードなどを提出したときに、その後になって、もっと良いアイデアが浮かんだという経験はありませんか。実のところ、私は後知恵の帝王です。幸い、そうした場合の対応にはブログがうってつけです。「The Baker’s Dozen」に関連する補足情報や追加のヒントについては、私のブログ(www.TheBakersDozen.net)をぜひチェックしてください。他にも、お得な情報が載っているかもしれませんよ。

過去の記事

著者紹介

Kevin S. Goff(Kevin S. Goff)
.NET、Visual FoxPro、SQL Server、Crystal Reportsによる独自のWebソリューション/デスクトップソリューションを提供するコンサルティンググループ「Common Ground Solutions」の創業者兼主任コンサルタント。ソフトウェアアプリケーションの開発経験は17年に及ぶ。米農務省からシステムオートメーション関連の賞をいくつか受賞。また、6桁の投資見返りを実現するソリューションを開発したことによりFortune 500企業から特別表彰を受ける。保険、会計、環境衛生、不動産、出版、広告、製造、金融、日用品、貿易振興など多様な業界に携わった経験を持ち、さまざまな形態で独自のトレーニングも行っている。

プリンター用
記事を転送
この記事をクリップ!
厳選した九州のお野菜とお米をお届け
厳選した九州のお野菜とお米をお届け 野菜の木では、老舗料亭 沙羅の木が厳選した九州のお野菜とお米をお届けします。 毎週、隔週での定期のご購入も可能です。 入会費、年会費、送料、荷造手数料は無料です。
注目のトピックス
Copyright 2012 internet.com K.K. (Japan) All Rights Reserved.