いくぶん前置きが長くなってしまったが、以降は.NETデータ・プロバイダのクラスを利用して、プログラムからデータベースを操作する方法を解説していく。
すでに述べているように、本連載ではMSDEをデータベースとして利用し、それに付属しているサンプル用のテーブルにアクセスする。よってプログラムでは、SQL Server用の.NETデータ・プロバイダを使用する。しかし、データ・プロバイダ固有の機能は、基本的に利用しないつもりなので、SQL ServerやMSDE以外のデータベースを利用する場合でも、プログラム中で使用している名前空間やクラスの名前を書き換えれば、ロジックや手順はそのままで対応できるはずだ。
まずは、データベースに対して、プログラムからselect文を発行し、テーブルからデータを取得してみる。
前回では、コマンドライン・ツール「osql.exe」によりpubsデータベースに接続し、select文によりpublishersテーブルから、pub_idとpub_nameの列を取り出した。今回ここではまず、osql.exeで行った同じ内容の処理を、ADO.NETを使ってコードで記述してみよう。
■SqlConnectionクラスによる接続
プログラムからデータベースにアクセスする場合、まずデータベースに接続する(データベースをオープンする)という作業が必要になる。今回で解説している.NETデータ・プロバイダを使用したデータベース・アクセスの処理は、すべて最初に明示的なデータベースの接続を必要とする。
ADO.NETでデータベースに接続するには、SqlConnectionクラスを利用する。
前回でosql.exeを使用してデータベースに接続する際、次のようなコマンドラインでosql.exeを起動した。
osql -S (local)\NetSDK -E -d pubs
ここではオプションとして最低限必要な次の3つのオプションを指定していた(厳密には、接続後にuse文を使用してデータベースは指定できるので、-dオプションは必須というわけではない)。
SqlConnectionクラスによりデータベースに接続するためには、これらと同じ内容の情報を「接続文字列」として、セミコロン区切りの1つの文字列で指定しなければならない。このための文字列は次のようなものになる。
string connStr = "Server=(local)\\NetSDK;"
+ "Trusted_Connection=yes;"
+ "database=pubs";
たいていの場合、この接続文字列は長ったらしくなる。そのため、ここでは分割して記述しているが、もちろん長い1つの文字列として記述しても問題ない。
また、この接続文字列では、同じ内容の項目を表すのにいくつかのバリエーションがある。例えばデータベース・サーバを指定している「Server」は、「Data Source」、「Address」、「Network Address」などでもOKだ。接続文字列中で使用可能な記述名は、リファレンス・マニュアルのSqlConnectionプロパティのConnectionStringプロパティの項目に列挙されているので参照していただきたい。
この接続文字列をSqlConnectionクラスのコンストラクタにパラメータとして指定し、SqlConnectionオブジェクトを作成する。そしてOpenメソッドを呼び出して、データベースとの接続を行う。これらのコードは次のようになる。
SqlConnection conn = new SqlConnection(connStr);
conn.Open();
//
// ここでデータベースにアクセスする
//
conn.Close();
データベースにアクセスし終えたらCloseメソッドを呼び出して、接続を閉じなければならない。データベースへの接続数は限られているので、可能な限り早く閉じるべきものだ。
■SqlCommandクラスによるコマンド作成
次に、データベースへ発行するselect文を、SqlCommandクラスにより用意する。次の例では、select文を記述した文字列と、上で作成したSqlConnectionオブジェクトの2つのパラメータをとるSqlCommandクラスのコンストラクタを使用して、SqlCommandオブジェクトを作成している。
string sqlStr = "SELECT pub_id, pub_name FROM publishers";
SqlCommand cmd = new SqlCommand(sqlStr, conn);
SqlCommandクラスのコンストラクタの第2パラメータとして指定たSqlConnectionオブジェクト(変数conn)による接続を通じて、select文がデータベースへ送信されることになる。
ただしこの時点では、まだ送信はされない。select文とSqlConnectionオブジェクトを装備したSqlCommandオブジェクトを作成しただけである。そのため、このコードを記述する位置はSqlConnectionクラスのOpenメソッドを呼び出す前でも後でも問題はない。
■SqlDataReaderクラスによる読み取り
続いて、SqlCommandオブジェクトに対してExecuteReaderメソッドを実行する。これによりselect文が送信され、データベース・サーバ上で処理される。
SqlDataReader dr = cmd.ExecuteReader();
ExecuteReaderメソッドは、データベース・サーバから返される処理結果を読み出すためのSqlDataReaderクラスのオブジェクトを返す。このオブジェクトについては、newによりインスタンスを作成しないところが、SqlConnectionクラスや、SqlCommandクラスと異なっている。ここでは、select文の処理結果(ここでは複数のレコードであるとする)を内部に格納したオブジェクトが、SqlDataReaderオブジェクトとしてExecuteReaderメソッドから返されると考えれば分かりやすいかもしれない。
次の図は、ここまでに登場した3つのオブジェクトの関連をまとめたものだ。
この図は、SqlConnectionオブジェクトによる接続を基に、SqlCommandオブジェクトのselect文が実行され、処理結果がSqlDataReaderオブジェクトにより読み出されることを示している。
■SqlDataReaderオブジェクトからのレコード取得
最後に、SqlDataReaderオブジェクトに格納されているレコード(行データ)を、SqlDataReaderクラスのReadメソッドにより、順に読み出していく。ただし、Readメソッドは取得したレコードを返さず、下の図のように内部で保持されている処理結果の現在のレコードを1つ進めるだけだ*1。
*1 実際の処理においては、ExecuteReaderメソッドを呼び出したときにすべての処理結果が取得されるのか、Readメソッドを呼び出すたびに取得されるのかは、データ・プロバイダの実装による。ここではExecuteReaderメソッドを呼び出した時点で、すべての処理結果の取得が完了しているという前提で図示している。
Readメソッドを何度か呼び出したあと、最後のレコードに達して、現在のレコードをそれ以上進めることができない場合にReadメソッドはfalseを返す(それまではtrueを返す)。また、最初の状態では現在のレコードは設定されておらず(=最初のレコードの前にセットされており)、最初にReadメソッドを呼び出した場合に、先頭のレコードが現在のレコードとなる。
現在のレコード上にある各カラム(各列)のデータは、上図にあるようにSqlDataReaderクラスのインデクサにより取得する。インデックス(添字)には、select文で指定したテーブルの列名を指定することができる。
ここでは、読み出したデータ(各レコードにはpub_idとpub_nameの2つのデータが存在する)を画面に表示してみよう。このための処理は、次のようなコードとなる。
while (dr.Read()) {
Console.Write("{0},{1}\n", dr["pub_id"], dr["pub_name"]);
}
dr.Close();
Readメソッドの呼び出しをwhile文の条件として記述するこのやり方は、SqlDataReaderオブジェクトからデータを取得するときの定石だ。
なお、.NET Frameworkバージョン1.1では、SqlDataReaderクラスにHasRowsプロパティが追加されている*2。このプロパティは、SqlDataReaderオブジェクトにレコードが格納されているかどうかを示すものだ。これまで、select文の結果の有無を調べるには、実際にReadメソッドを呼び出してみるしかなかった。しかしこうすると、現在のレコードが先頭レコードにセットされてしまうため少し不便なところがあった。HasRowsプロパティがあれば、処理結果中のレコードの有無による処理の場合分けをシンプルに記述することができる。
*2 このHasRowsプロパティの追加以外で.NET Frameworkバージョン1.1で追加された新機能は、すでに述べたODBC用とOracle用の.NETデータ・プロバイダの追加と、各データ・プロバイダのXXXConnectionクラスのEnlistDistributedTransactionプロパティの追加である。
ところで、Readメソッドは前方向へのみレコードを読み進めるだけなので、過ぎてしまった前のレコードをもう1度読みたい場合にはどうすればよいのだろうか。このような場合には、基本的に、再度ExecuteReaderメソッドを呼び出して最初のレコードから読み直すか、あらかじめ自前でレコードを保存しておくしかない。
SqlDataReaderクラスの役割は、レコードの読み取りだけを高速かつ効率的に行うことであり、そのためこのような少々不便とも思える設計となってしまっている。
ちなみに、処理結果をあらかじめ自前でレコードを保存しておくといま述べたが、まさにこの仕組みを実現しているのが、今回の冒頭でも触れているDataSetクラスである。自前で保存しておいたレコードを更新し、それをデータベースに書き戻すといったような処理が必要ならば、DataSetクラスの利用を検討すべきだ。
Copyright© Digital Advantage Corp. All Rights Reserved.