いま「データベース更新のパターンその3」で示した、データベースをロックしないで同時実行を制御する方式は「オプティミスティック同時実行制御」と呼ばれる(オプティミスティック(optimistic)は「楽観的な」という意味)*1。ほかのだれかが更新しようとしているかもしれないけれど、取りあえず更新を実行してしまおうという方式だ。
*1 「データベース更新のパターンその1」もデータベースをロックしないためオプティミスティック同時実行制御の一種といえる。ちなみに、データベースのロックを用いるパターン「その2」はペシミステック同時実行制御(悲観的同時実行制御)と呼ばれる。ロックしているため更新は失敗しない。
データベースをロックせずに「楽観的」に更新しているので、当然ながら同時に更新を実行しようとしている競合する処理がほかにある場合には、それらのいくつかは失敗する。それでは、競合する処理がない場合には正しく更新し、競合する処理がある場合には失敗するようなupdate文はどのようなものになるだろうか。
SQL文を自動生成するSqlCommandBuilderクラスは、ちょうどそのようなupdate文を生成してくれる。まず、SqlCommandBuilderクラスを使って先ほどのサンプル・プログラム(updateds.cs)を書き直すと次のようになる。
// cbupdateds.cs
using System;
using System.Data;
using System.Data.SqlClient;
public class BuilderUpdateFromDataSet {
public static void Main() {
string connStr = "Server=(local)\\NetSDK;"
+ "Trusted_Connection=yes;"
+ "database=pubs";
string selectStr
= "SELECT pub_id, pub_name FROM publishers";
// 接続用オブジェクトの作成
SqlConnection conn = new SqlConnection();
conn.ConnectionString = connStr;
// select用コマンド・オブジェクトの作成
SqlCommand selectCmd = new SqlCommand();
selectCmd.Connection = conn;
selectCmd.CommandText = selectStr;
// データアダプタの作成
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = selectCmd;
// データセットへの読み込み
DataSet ds = new DataSet();
da.Fill(ds, "publishers");
DataTable dt = ds.Tables["publishers"];
// 主キーの設定
dt.PrimaryKey = new DataColumn[] { dt.Columns["pub_id"] };
// データの更新
DataRow targetRow;
targetRow = dt.Rows.Find("0736");
targetRow["pub_name"] = "新月書店";
targetRow = dt.Rows.Find("1756");
targetRow["pub_name"] = "ラモーナ出版";
// コマンド自動作成
SqlCommandBuilder cb = new SqlCommandBuilder(da);
// データベースの更新
da.Update(ds, "publishers");
}
}
// コンパイル方法:csc cbupdateds.cs
2つのレコードの内容を変更しているため、このプログラムの場合にも実行時には2つのupdate文が発行される。
publishersテーブルのデータがサンプル・データのインストール直後の状態でこのプログラムを実行したとき、データベースで実際に実行される最初のupdate文をSQLプロファイラで調べると次のようになっている(名前付きパラメータによる値の受け渡しは省略(展開)している)。
UPDATE publishers
SET pub_name = '新月書店'
WHERE
(pub_id = '0736')
AND
((0 = 1 AND pub_name IS NULL) OR (pub_name = 'New Moon Books'))
少々ややこしいが、最後の行にある常に成立しないはずの「0 = 1」という条件を含んだ括弧部分を省くと、これは次のように書き換えることができる。
UPDATE publishers
SET pub_name = '新月書店'
WHERE (pub_id = '0736') AND (pub_name = 'New Moon Books')
このupdate文は、更新しようとするレコードにおいて、更新しようとするpub_nameカラムのデータが、データセットに読み込んだときの値である「New Moon Books」であればそれを「新月書店」に書き換えるというものだ。
これはすなわち、レコードのカラムの値が書き換える前の状態のままである場合にのみ、レコードを更新するということを意味している。もし、ほかのユーザーがすでに「New Moon Books」を、例えば「ニュームーン・ブックス」と書き換えていたら、このupdate文は失敗する。これが、ADO.NETで標準的に使用されるオプティミスティック同時実行制御のための1つの方式である。
ただし、ここでは更新対象となっているカラム(pub_name)の値にしか注目していない。このため、場合によっては不完全なオプティミスティック同時実行制御となることがある。ほかのユーザーによってpub_nameカラム以外のカラムが更新されていたとしても、pub_nameカラムの更新が成功してしまうためだ。より厳密には、すべてのカラムの値をデータセットに取り込んでおき、更新時にはすべてのカラムの値に変化がないことをチェックしなければならない*2。
*2 たくさんのカラムを持ったテーブルの場合には、当然ながら長いwhere句が作成される(作成する)ことになる。リファレンス・マニュアルのオプティミスティック同時実行制御の項目では、これに代わる方法としてタイムスタンプ列(timestamp列)を用いる方法が紹介されているので参照しておいてほしい。この方法を利用すれば、where句でチェックすべきカラムは主キーとタイムスタンプ列だけでよい。
■DataRowオブジェクトの複数のバージョン
ところで、前述のサンプル・プログラム(cbupdateds.cs)では、次のコードによりDataRowオブジェクト内の値を「New Moon Books」から、「新月書店」に上書きしている。
targetRow["pub_name"] = "新月書店";
にもかかわらず、データベースに送信されたupdate文には、変更前の値である「New Moon Books」という文字列が含まれていることに疑問を持たれた方がいるかもしれない。実はDataRowオブジェクトは、更新されて新しくなった現在のレコードの内容に加え、更新前の元のレコードの内容も保持している(実際には、キャンセル可能な更新のための「提示された値」も持っている。これについてはリファレンス・マニュアルの「行の状態とバージョン」などを参照していただきたい)。
レコードの各バージョン(現在、更新前、または提示時点)の値には、DataRowクラスの2つのパラメータを取るインデクサによってアクセスすることができる。例えばレコードの元の値は、targetRow["pub_name", DataRowVersion.Original]のようにDataRowVersion列挙体のOriginal値を第2パラメータに指定して取得できる。
SQL文で名前付きパラメータを使用するためにSqlParameterクラスを利用する場合には、SourceColumnプロパティによりデータテーブル内の対応するカラムを指定していた。このときには、SourceVersionプロパティにDataRowVersion列挙体の値を設定すれば、パラメータの値として使用されるレコードのバージョンを指定できる。
■カラムの値がNullの場合の更新
自動生成されたupdate文については、もう少し解説しておく必要がある。先ほど解説を省いた、次のSQL文の太字部分だ。
UPDATE publishers
SET pub_name = '新月書店'
WHERE
(pub_id = '0736')
AND
((0 = 1 AND pub_name IS NULL) OR (pub_name = 'New Moon Books'))
この太字部分は何のためにあるのだろうか。
ちなみに、名前付きパラメータを展開していない、実際にデータベースに送信されているupdate文は次のようなものだ。
UPDATE publishers
SET pub_name = @p1
WHERE
(pub_id = @p2)
AND
((@p3 = 1 AND pub_name IS NULL) OR (pub_name = @p4))
名前付きパラメータである「@p1」から「@p4」に置き換わる実際の値も、update文といっしょにデータベースに送信される。問題は「@p3」に入る値である(上記の場合にはこの値は「0」)。
この問題を解く鍵は、publishersテーブルではpub_nameカラムが「Nullを許容」として設定されているということにある。つまり、pub_nameカラムの値にはNull値を代入できるということだ。Null値というのは、空の文字列とは異なり、それのカラムには値が設定されていないということを意味する特別な値である。
いま更新対象としているレコードで、すでにpub_nameカラムの値がNull値になっているとしよう。これを「新月書店」という値で更新しようとする場合、データベースに送信される自動生成されたupdate文の実際の内容は次のようになる。
UPDATE publishers
SET pub_name = '新月書店'
WHERE
(pub_id = '0736')
AND
((1 = 1 AND pub_name IS NULL) OR (pub_name = NULL))
この場合には「@p3」の値は「1」になっている。「1 = 1」は当然常に成立するので、今回の太字の部分は有効だ。
一方、最後の「pub_name = NULL」は常に有効ではない。なぜなら、あるカラムの値がNull値かどうかを判別するには「=」ではなくて「IS」を使用しなくてはならないからだ。よって、上記のupdate文は実質的に次のupdate文と同じだ。
UPDATE publishers
SET pub_name = '新月書店'
WHERE (pub_id = '0736') AND (pub_name IS NULL)
どうやら問題の「@p3」は「pub_name IS NULL」という条件を有効にしたり無効にしたりするためのスイッチの役割を持っているようである。自動生成されたupdate文は、既存レコードのカラムの値がNull値であっても、通常の値であっても、セットする名前付きパラメータの値だけで両方に対応できるようになっているということである。
なお、.NET Framework 1.0のSqlCommandBuilderクラスが自動生成するwhere句は次のようなパターンのものだったが、.NET Framework 1.1で少し変更されたようだ。
((pub_name IS NULL AND 'New Moon Books' IS NULL) OR (pub_name ='New Moon Books'))
Copyright© Digital Advantage Corp. All Rights Reserved.