Visual Basic データベース講座
VB2005 対応

 

Visual Basic 中学校 > データベース講座 >

第4回 DataTableの利用

2006.12.9

データをまとめて取得して表形式で表示する方法を説明します。さらに、変更点をデータベースに反映させる方法も説明します。

概要

・DataTableを使用すると表形式で複数のデータをまとめて保持できる。

・DataTableにデータを読み込むにはOleDbDataAdapterのFillメソッドを使用する。

・DataTableの個々のレコードはバージョン管理されており、値を変更した後でも変更前の内容を取得することができる。

・DataTableへの変更点をデータベースに反映させるには個々のレコードの値を基にSQL文を使用する。

 

1.もっと効率的に

 

前回は効率性の観点は無視してとにかくデータベースの読み書きが自由にできるようになるべく説明をしました。今回はこれに引き続いて効率性を考慮に入れていきます。

前回の方法では読み込む場合も書き込む場合も対象の値をピンポイントで指定していた点が最も効率が悪いと言えます。1回の処理で1つの値だけを対象にするようなまれなケースであれば前回の方法でもよいのですが、たいていの場合はある程度まとまったデータを扱うことになります。

今回はDataTableクラスを使用してまとまりとしてデータを扱う方法を説明します。そして、今回の説明を読んでいただければDataTableにはそれ以上のメリットがあることもお分かりいただけるでしょう。実際のところDataTableはVBにおけるデータベースプログラミングで中心となる最重要のクラスなのです。

 

2.データの抽出

 

前回紹介したプログラムをもう一度眺めてみます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
Dim SQLCm As OleDbCommand = Cn.CreateCommand

SQLCm.CommandText =
"SELECT 説明 FROM T_目マスタ WHERE 目ID = 2"

Dim Value As String

Cn.Open()
Value = SQLCm.ExecuteScalar
Cn.Close()

MsgBox(Value)

■リスト1:[T_目マスタ]テーブルから、目ID=2のレコードの[説明]を抽出する。

プログラムを実行するためにはプログラムの一番先頭にImports System.Data.OleDbと記述する必要がある点も忘れないでください。

このプログラムは[T_目マスタ]テーブルから値を1つだけ取得するプログラムです。「値が1つだけ」というのはレコードと列が指定されていることから保障されます。

ところで、SQLのSELECT文ではレコードを指定しないことも可能で、レコードを指定しない場合このプログラムは次のように書き換えられます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
Dim SQLCm As OleDbCommand = Cn.CreateCommand

SQLCm.CommandText =
"SELECT 説明 FROM T_目マスタ"

Dim Value As String

Cn.Open()
Value = SQLCm.ExecuteScalar
Cn.Close()

MsgBox(Value)

■リスト2:T_目マスタのすべてのレコードの[説明]を取得しようとするが文字列型で受けるので1つしか受け取れない。

こうすると、列は指定されていますがレコードは指定されていない状態になりますので、[T_目マスタ]テーブルの[説明]列のすべての値が選択対象となります。

それでもExecuteScalarメソッドは一番最初の値だけを処理するので結果として表示される値は1つだけです。複数の値をまとまりとして処理するにはExecuteScalarメソッドとは別の方法でデータを取得する必要があります。

次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

'▼データ取得
Dim Cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
Dim SQLCm As OleDbCommand = Cn.CreateCommand
Dim Adapter As New OleDbDataAdapter(SQLCm)
Dim Table As New DataTable

SQLCm.CommandText =
"SELECT 説明 FROM T_目マスタ"
Adapter.Fill(Table)

'▼値の表示
Dim Value As String

'先頭の値を表示
Value = Table.Rows(0)("説明")
MsgBox(Value)

'20番目の値を表示
Value = Table.Rows(19)("説明")
MsgBox(Value)

'▼後処理
Table.Dispose()
Adapter.Dispose()
SQLCm.Dispose()
Cn.Dispose()

■リスト3:T_目マスタのすべてのレコードの[説明]を取得する。

今までのプログラムとは大分異なります。一番注目してほしいのはデータをまとまりとして保持するためにDataTableを使用している点です。普通の文字列型の変数では値を1つしか受け取れませんが、DataTableは「表」のようなイメージのクラスで、複数の値をまとめて取り扱うことができます。まさにデータベースからデータのまとまりを取得して保持するのに最適なクラスです。

 DataTableをデータで満たすにはOleDbDataAdapterクラスのFillメソッドを使用します。Fillメソッドを使用したときにどのようなデータを取得するのかは前回と同様OleDbCommandクラスのCommandTextにSQL文の形式で指定します。

MsgBoxでは複数の 値を同時に表示するのが面倒なので、この例では値が正しく取得できている確認するために2つのMsgBoxを使用しました。

小さな違いとしてFillメソッドを使用する場合には接続を開いたり閉じたりしなくていい点にもちょっとだけ注目してください。ExecuteScalarExecuteNonQueryの際には自分で接続を開いたり閉じたりしていましたがFillはこういった処理を自動でやってくれます。

このままでは値の確認に不便なので、値の一覧を見られるようにDataGridViewコントロールを使用しましょう。ツールボックスからDataGridViewを選択してフォームに貼り付けてください。VB.NET2002, VB.NET2003ではDataGridViewがないので代わりにDataGridを使用し、以下の説明では適宜DataGridViewDataGridに読み替えてください。ただし、DataGridではサポートされていない機能もありますのでその場合は悪しからずご了承ください。

DataGridViewを配置したら上記のプログラムの値の表示部分を改造して次のようにします。

VB2005対応

'▼データ取得
Dim Cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
Dim SQLCm As OleDbCommand = Cn.CreateCommand
Dim Adapter As New OleDbDataAdapter(SQLCm)
Dim Table As New DataTable

SQLCm.CommandText =
"SELECT 説明 FROM T_目マスタ"
Adapter.Fill(Table)

'▼値の表示
DataGridView1.DataSource = Table

'▼後処理
Table.Dispose()
Adapter.Dispose()
SQLCm.Dispose()
Cn.Dispose()

■リスト4:T_目マスタのすべてのレコードの[説明]を取得・表示する。

これで実行すると今度はフォーム上のDataGridViewに取得したデータの一覧が表示されます。この一覧は修正・追加・削除が可能ですがいくら変更してもDataTableが保持している値が変更されるだけで実際のデータベースの値が変更されるわけではありません。

このことは初心者のうちはよく誤解している人がいるので注意してください。DataTableが記録しているレコードはすべてクライアントのメモリ上にあるだけで、いくらいじってもこのメモリの内容が書き換わるだけです。取得元のデータベースを更新するには更新用のプログラムを別に書く必要があります。そのプログラムは後半に紹介します。

 

ところで、 折角一覧表示しているのに[説明]列だけではさびしいのでSQLのSELECT文を改造してもっといろいろな列を同時に取得できるようにしましょう。いくつか例を示しますのでSQL文の練習のつもりで実際に実行してみてください。

次のように改造すると[目名]と[説明]の両方を取得できます。


SELECT 目名, 説明 FROM T_目マスタ
 

■SQL1:T_目マスタの[目名]と[説明]を取得する。

次の用例では[目名]と[読み]と[説明]を取得できます。


SELECT 目名, 読み, 説明 FROM T_目マスタ
 

■SQL2:T_目マスタの[目名]と[読み]と[説明]を取得する。

このようにSELECT文ではカンマでつなげることによって必要な列を複数記述することができます。列名はSELECT節に記述します。

次の例では[T_目マスタ]テーブルのすべての列を取得できます。

SELECT * FROM T_目マスタ

■SQL3:T_目マスタのすべてを取得する。

記号「*」(読み方:* = アスタリスク)を使用すると「すべての列」という意味になります。これは何かと重宝します。ここからの説明ではSQL文はこの最後のSELECT * FROM T_目マスタを使用していることを前提にします。

■画像1:すべて取得したときの画面 

 

3.DataTableの構造

 

ここまで実際にやってみるとわかると思いますが、DataTableクラスとは結局のところテーブルそのものなのです。データベースからデータをまとめて持ってきてクライアントのメモリ上に新しいテーブルとしてデータを展開したものがDataTableなのです。

したがってDataTableから特定の値を抽出するためにはやはりDataTable内の行と列を指定します。これらもクラス化されていて行を表しているのはDataRowクラス、列を表しているのはDataColumnクラスです。特に重要なのは行をあらわすDataRowクラスです。DataTableの 実体は複数のDataRowで構成されていると考えることもできます。DataColumnクラスの方はそれほど出番は多くありません。

個々のDataRowにアクセスするにはDataTableRowsプロパティを使用します。RowsプロパティはDataRowのコレクションになっていて任意のDataRowにアクセスすることができます。たとえば、3番目のDataRowを取得するには次のように書きます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Row As DataRow

Row = Table.Rows(2)

■リスト5:3番目のレコードを取得する。

そして、この行の[目名]の値を取得するには、次のように書きます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Row As DataRow
Dim Value As
String

Row = Table.Rows(2)
Value = Row("目名")

■リスト6:3番目のレコードの[目名]を取得する。

ここで列はDataColumnクラスではなく文字列で指定しているのでDataColumnクラスの出番はDataRowクラスほど多くないわけです。

上記の例は次のように短く書くこともでき、この書き方のほうが広く使われているようです。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Value As String

Value = Table.Rows(2)("目名")

■リスト7:3番目のレコードの[目名]を取得する。

どの書き方でもループを回すことによってすべての列を処理できるという点は重要です。

列名も文字列ではなく番号でしていることもできます。次の例では3行目の5列目の値を取得します。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Value As String

Value = Table.Rows(2)(4)

■リスト8:列を数字で指定する。

 

行の数はDataTable.Rows.Countで取得できます。列の数はDataTalbe.Column.Countで取得できますし、DataTable.RowsDataTable.Columnもそれぞれ行と列をあらわすコレクションですから簡単にすべてのデータを対象にしたループを記述することができます。

利用例として取得した内容をCSVファイルに書き込むプロうグラムを紹介します。この例ではレコードははFor Eachで、列はForでループさせています。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Writer As New IO.StreamWriter("C:\DataTableTest.csv")
Dim LineText As
String

For Each Row As DataRow In Table.Rows

    LineText =
""

   
For i As Integer = 0 To Table.Columns.Count - 1
        If Len(LineText) > 0
Then
           
LineText &= ","
       
End If
       
LineText &= Row(i)
   
Next

   
Writer.WriteLine(LineText)

Next

Writer.Close()
Writer.Dispose()

■リスト9:DataTableの内容をCSV形式で出力する。

ファイルに文字を書き込むプログラムならどの場合でも当てはまることですが、文字コードを指定しなかった場合VBではUTF-8でファイルに書き込みます。ShiftJISなどで書き込みたい場合はStreamWriterのコンストラクタで文字コードを指定してください。たとえば、次のようになります。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Dim Writer As New IO.StreamWriter("C:\DataTableTest.csv", False, System.Text.Encoding.GetEncoding("Shift-Jis"))

■リスト9-1.ShiftJISで書き込む場合

なお、対応していない文字コードをメモ帳など開くと文字化けして表示されます。新しいWindowsではUTF-8が標準で使用されています。

CSVではなくxml形式で出力するだけならWriteXmlメソッドがはじめから用意されているので簡単にできます。

VB.NET2002対応 VB.NET2003対応 VB2005対応

Table.TableName = "T_目マスタ"
Table.WriteXml("C:\DataTableTest.xml")

■リスト10:DataTableの内容をXML形式で出力する。

WriteXmlメソッドを使用するには事前にDataTableにテーブル名を設定する必要があります。テーブル名はコンストラクタで指定できるのですが、ここではプロパティで指定する例を紹介しました。

 

なお、一度DataGridViewにセットした後では次のようにしてDataGridViewからDataTableを取得することができるので、DataTable型の変数に広い適用範囲を持たせて使いまわす必要はありません。

VB2005対応

Dim Table As DataTable

Table = DirectCast(DataGridView1.DataSource, DataTable)

■リスト11:DataGridViewDataSourceからDataTableを取得する。

このようにしてDataGridViewに表示されている内容を自在に制御することもまったく簡単にできるわけです。

 

4.DataTableの修正・削除・新規追加

 

今度はDataTableを使ったデータベースの更新を説明しましょう。DataTable内の値を書き換えること 自体は特に説明は必要ないでしょう。連結されたDataGridViewを使ってユーザーの操作で値を書き換えることもできますし、プログラムを使って書き換えることもできます。

次の例はDataTableの12行目の説明を書き換えます。ここではDataTableDataGridViewから取得しています。

VB2005対応

Dim Table As DataTable = DirectCast(DataGridView1.DataSource, DataTable)
Dim Row As DataRow

Row = Table.Rows(11)
Row("説明") =
"イワダヌキ, ハイラックス"

■リスト12:12行目のレコードの値を書き換える。データベースは更新されない。

もちろん、この例は次のように簡略化して書くこともできます。

VB2005対応

Dim Table As DataTable = DirectCast(DataGridView1.DataSource, DataTable)

Table.Rows(11)("説明") =
"イワダヌキ, ハイラックス"

■リスト13:12行目のレコードの値を書き換える。データベースは更新されない。

さらに簡略化して1行で書くこともできます。実際に何行で書くかは好みと可読性を考慮して決めてください。私はここでは各クラスの働きがわかるようにあまり簡略化しないで書いていますが、実際にプログラムするときにはもっと簡単に書いています。

DataTableから行を削除するにはDeleteメソッドを使用します。

VB2005対応

Dim Table As DataTable = DirectCast(DataGridView1.DataSource, DataTable)
Dim Row As DataRow

Row = Table.Rows(11)
Row.Delete()

■リスト14:12番目のレコードを削除する。データベースは更新されない。

DataTable.Rows.Removeメソッドも日本語上は「行を削除する」機能がありますが、上述のDeleteメソッドとは明らかに働きが異なります。すぐ後で説明しますがDeleteメソッドは実際には「削除された」というしるしをつけるだけでDataRowクラス自体は消滅しないのですが、DataTable.Rows.RemoveメソッドのほうはDataRowクラス自体を消してしまいます。データベース更新のための削除にはDeleteの方が都合がよいです。

レコードの新規追加は少し手順が必要です。まず、テーブルと同じ構造をもつDataRowを作成します。この段階ではこの新しいDataRowは独立していてどのテーブルにも属していません。2つ目のステップでこのDataRowDataTableに含めます。

次のようになります。

VB2005対応

Dim Table As DataTable = DirectCast(DataGridView1.DataSource, DataTable)
Dim Row As DataRow

'同じ構造をもつ独立したレコードを作成
Row = Table.NewRow

'レコードに値を設定
Row("目ID") = 900
Row("目名") =
"獣脚目"
Row("読み") = "ジュウキャクモク"
Row("綱ID") = "4" '爬虫類
Row("説明") = "ティラノサウルス, アロサウルス, ディノニクス"

'レコードをDataTbleに含める
Table.Rows.Add(Row)

■リスト15:新規レコードを追加する。データベースは更新されない。

これでDataTableに対するレコードの修正、削除、新規追加ができるようになりました。どれもごく簡単にできることが理解いただけたでしょう。しかし、先にも説明したようにDataTableに対する変更はあくまでもクライアントのメモリ上の変更に過ぎず、DataTableを変更したからと言ってそのもととなっているデータベースのテーブルの内容が変更されるということは一切ありません。

次の節ではDataTableの変更をデータベースに反映させる方法を説明します。

 

5.変更点のデータベースへの反映

 

レコードをあらわすDataRowクラスは常に最初の状態を保存していますし、そのレコードに対してどのような編集が加えられたかも記録しています。ですから、削除されたレコードは削除するようにデータベースに指示できますし、更新したレコードは更新するように指示することもできるわけです。この仕組みを順を追ってみていきましょう。

まず、レコードを編集した後で編集前の値を取得し、そのレコードが編集されたことを示す例を紹介します。この例の半分は今まででてきた例を組み合わせたものですが全体としては示唆に富みます。

VB2005対応

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

   
'▼データ取得
   
Dim Cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
    Dim SQLCm As OleDbCommand = Cn.CreateCommand
    Dim Adapter As New OleDbDataAdapter(SQLCm)
    Dim Table As New DataTable

    SQLCm.CommandText =
"SELECT * FROM T_目マスタ"
   
Adapter.Fill(Table)

   
'▼値の表示
   
DataGridView1.DataSource = Table

   
'▼後処理
   
Table.Dispose()
    Adapter.Dispose()
    SQLCm.Dispose()
    Cn.Dispose()

End
Sub

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

   
'レコードを編集する
   
Dim Table As DataTable = DirectCast(DataGridView1.DataSource, DataTable)
    Dim Row As DataRow

    Row = Table.Rows(11)
    Row("説明") =
"イワダヌキ, ハイラックス"

End Sub

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click

    Dim Table As DataTable = DirectCast(DataGridView1.DataSource, DataTable)
    Dim Row As DataRow = Table.Rows(11)
    Dim OldValue As String
'編集前の値
   
Dim NewValue As String '編集後の値

   
'レコードが編集されているか判定する
   
If Row.RowState <> DataRowState.Modified Then
       
MsgBox("このレコードは編集されていません。")
       
Return
   
End If

   
OldValue = Row("説明", DataRowVersion.Original)
    NewValue = Row(
"説明")

    MsgBox("編集前:" & OldValue & vbNewLine & "編集後:" & NewValue)

End
Sub

■リスト16:変更したレコードの変更前の値と編集後の値を取得・表示する

フォームの表示時にはすでに[T_目マスタ]テーブルの値が表示されています。この状態でButton2をクリックすると「レコードは編集されていません。」と表示されます。この例では説明を簡単にするために常に12番目のレコードだけを対象にしている点にも注意してください。12番目のレコードは標準の状態では「岩狸目(イワダヌキ)」です。

DataGridViewを直接編集して岩狸目の説明を何か別のものに変更してからButton2をクリックすると今度は編集前の値と編集後の値が対比して表示 されます。Button1を利用してプログラムから値を変更した後でButton2をクリックしても同様です。

このプログラムには2つのポイントがあります。まず、レコードに対してどのような編集が行われたか、または行われていないかを示すDataRow.RowStateプロパティです。RowStateプロパティの値は次のどれかでありレコードに対する編集状態を取得することができます。

読み方 意味
Added アディド 新規追加されたレコード
Deleted デリーテッド 削除されたレコード
Detached デタッチド テーブルに属していないレコード
Modified モディファイド 修正されたレコード
Unchanged アンチェンジド 変更されていないレコード

上記の例ではIf Row.RowState <> DataRowState.Modified Thenというコードでレコードが編集されているかされていないかを識別しています。もちろんこの識別方法は不十分で、これでは新規追加されたレコードや削除されたレコードを検出できません。

もう1つのポイントはDataRowのバージョン管理機能 です。編集前の値と編集後の値の両方を取得できるのはこの機能によるものです。

普通に Value = Row("説明") と記述した場合は現在の値が取得できるだけですが、第2引数でバージョンを指定することにより編集前の値などを取得できます。なお、Row("説明")という書き方はRow.Item("説明")の省略形なのでこの機能のヘルプを探すときはItemプロパティを見てみてください。

指定できるバージョンは次のとおりです。

読み方 意味
Current カレント 現在の値
Default デフォルト 通常はCurrentと同じ。RowStateDetachedの場合はProposedと同じ。
Original オリジナル 編集前の値
Proposed プロポーズドゥッ 編集中の値

バージョンの指定を省略した場合はDefaultが指定されたものとみなされます。

これらのバージョンの値はいつでも使用できるわけではありません。たとえば、削除されたレコードに対してCurrentDefaultを取得しようとするとバージョンが存在しないため例外VersionNotFoundExceptionが発生します。Proposedはもっと限定的なシーンでのみ利用可能です。DataRowHasVersionメソッドを使用すると 特定のバージョンが存在するか調べることができます。

 

修正・新規追加などの状態を一旦確定させるためにDataTableにはAcceptChangesメソッドも用意されています。AcceptChangesを呼び出すと、それぞれのレコードに加えられた変更はそのまま温存しますが、すべてのDataRowRowStateプロパティはUnchangedになり、その時の値がOriginalバージョンの値になります。特定のレコードだけUnchangedにするためにDataRowにもAcceptChangesメソッドが用意されています。

AcceptChangesが各RowStateに及ぼす影響を表にまとめておきます。

AcceptChanges AcceptChanges
Added Unchanged
Deleted (削除される)
Modified Unchanged

データベースへの更新後に引き続き編集を行う可能性がある場合などは更新時点でAcceptChangesメソッドを呼び出しておくことで以降の更新も最初の更新と同じロジックで処理することができるようになります。もし、AccesptChangesメソッドを使用しないとしたら1回目の更新対象となったレコードは2回目の更新対象からは省くという処理が必要になりかなり大変です。もっともこれは一度の呼び出しで何度も更新する可能性がある場合の話で、更新後にすぐに画面を閉じたり、更新後にすぐに新たなDataTableを取得するなどの仕様であれば特に気にする必要はありません。

メモ:AcceptChangesが実際に役に立つシーンはリスト18の後で紹介します。

 

さて、レコードが編集されたかされていないか、レコードの編集前の値は何か、これら2点が取得できるようになったとしてDataTableの内容をデータベースに書き込むのに何の役に立つのでしょうか。

データベースの更新を行うときは前回も登場したようにSQL文を使用します。修正を意味するUPDATE、新規追加を意味するINSERT、削除を意味するDELETEです。

これで気がつかれたと思いますが、RowStateプロパティに応じて実行するSQL文を分岐させるわけです。

先ほどから登場している12番目のレコードである「岩狸目」だけに限定するとプログラムは次のようになります。岩狸目を修正・削除してこのコードを実行するとデータベースに修正・削除の内容が反映されるので実行の際には注意してください。

VB2005対応

Dim Table As DataTable = DirectCast(DataGridView1.DataSource, DataTable)
Dim Row As DataRow
Dim SQL As
String = ""

Row = Table.Rows(11)

'●SQL文の生成

Select
Case Row.RowState
   
Case DataRowState.Added
       
'▼新規追加されたレコードの場合
       
SQL = "INSERT INTO T_目マスタ VALUES ("
       
SQL &= Row("目ID") & ", "        '目ID
       
SQL &= "'" & Row("目名") & "', " '目名
       
SQL &= "'" & Row("読み") & "', " '読み
       
SQL &= Row("綱ID") & ", "        '綱ID
       
SQL &= "'" & Row("説明") & "' "  '説明
       
SQL &= ")"

   
Case DataRowState.Deleted
       
'▼削除されたレコードの場合
       
SQL = "DELETE FROM T_目マスタ WHERE "
       
SQL &= " 目ID = " & Row("目ID", DataRowVersion.Original)

   
Case DataRowState.Modified
       
'▼修正されたレコードの場合
       
SQL = "UPDATE T_目マスタ SET "
       
SQL &= " 目ID = " & Row("目ID") & ", "
       
SQL &= " 目名 = '" & Row("目名") & "', "
       
SQL &= " 読み = '" & Row("読み") & "', "
       
SQL &= " 綱ID = " & Row("綱ID") & ", "
       
SQL &= " 説明 = '" & Row("説明") & "' "
       
SQL &= " WHERE "
       
SQL &= " 目ID = " & Row("目ID", DataRowVersion.Original)

    Case
Else
       
Return

End Select

'●更新実行

Dim Cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
Dim SQLCm As OleDbCommand = Cn.CreateCommand

SQLCm.CommandText = SQL
Cn.Open()
SQLCm.ExecuteNonQuery()
Cn.Close()

Table.Dispose()
SQLCm.Dispose()
Cn.Dispose()

■リスト17:12番目のレコードへの修正・削除をデータベースに反映させる。

「岩狸目」はすでに存在するので新規追加というのはあり得ないのですが参考のために載せておきました。これで、DataGridViewを使って岩狸目を修正、削除してこのコードを実行すると実際にデータベースにその内容が反映されます。

注意  - DELETEとUPDATE

SQL文ではWHERE句を使って対象のレコードを指定します。WHERE句を省略した場合はそのテーブル内にある全レコードが対象となります。

そのためWHERE句を間違ったり指定しないでDELETE文を実行するとすべてのレコードが削除されてしまう場合があります。UPDATE文でもすべてのレコードが更新されてしまう場合があり元のデータを復元できないという点ではDELETEしたのと変わりありません。

DELETEUPDATEを使用する際は常にWHERE句の条件に気をつけて意図しないレコードまで対象に含まないように注意してください。

 

 

岩狸目だけでなくすべてのレコードを対象にしたい場合は上記のコードをループ内に入れます。次のようになります。

VB2005対応

Dim Cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
Dim SQLCm As OleDbCommand = Cn.CreateCommand
Dim Table As DataTable = DirectCast(DataGridView1.DataSource, DataTable)

For
Each Row As DataRow In Table.Rows

    Dim
SQL As String =
""

   
'●SQL文の生成

   
Select Case Row.RowState
       
Case DataRowState.Added
           
'▼新規追加されたレコードの場合
          
 SQL = "INSERT INTO T_目マスタ VALUES ("
           
SQL &= Row("目ID") & ", "        '目ID
           
SQL &= "'" & Row("目名") & "', " '目名
           
SQL &= "'" & Row("読み") & "', " '読み
           
SQL &= Row("綱ID") & ", "        '綱ID
           
SQL &= "'" & Row("説明") & "' "  '説明
           
SQL &= ")"

       
Case DataRowState.Deleted
           
'▼削除されたレコードの場合
           
SQL = "DELETE FROM T_目マスタ WHERE "
            SQL &= " 目ID = " & Row("目ID", DataRowVersion.Original)

       
Case DataRowState.Modified
           
'▼修正されたレコードの場合
           
SQL = "UPDATE T_目マスタ SET "
           
SQL &= " 目ID = " & Row("目ID") & ", "
           
SQL &= " 目名 = '" & Row("目名") & "', "
           
SQL &= " 読み = '" & Row("読み") & "', "
           
SQL &= " 綱ID = " & Row("綱ID") & ", "
           
SQL &= " 説明 = '" & Row("説明") & "' "
           
SQL &= " WHERE "
           
SQL &= " 目ID = " & Row("目ID", DataRowVersion.Original)

        Case
Else
           
Continue For

   
End Select

   
'●更新実行

   
SQLCm.CommandText = Sql

    Cn.Open()
    SQLCm.ExecuteNonQuery()
    Cn.Close()

Next

Table.Dispose()
SQLCm.Dispose()
Cn.Dispose()

■リスト18:すべてのレコードへの変更点をデータベースに反映させる。

このコードをFormClosingイベントなどに記述した場合は問題ないのですが、ButtonClickイベントなどに書いた場合には1つ考慮すべきことがあります。新しくレコードを追加した状態でデータベースへの更新を複数回実行するとエラーが発生するのです。

エラーの原因はレコードのキーの重複です。つまり、[T_目マスタ]テーブルでは同じ目IDのレコードは1つしか登録できないので、2つ以上登録しようとするとエラーになるのです。

たとえば新しく目IDが1000のレコードを追加したとしましょう。一度目の更新ではこのレコードはうまくデータベースに登録されます。目ID=1000のデータがはじめは存在しないからです。

次に、画面を閉じないでそのまま目IDが1001のレコードを追加したとします。そして、もう一度更新を実行すると今度はエラーになってしまいます。2回目の更新では目ID=1000のレコードと目ID=1001のレコードの両方を追加しようとするからです。1001の方は問題ないのですが、1000の方は先ほど自分で登録しているのでエラーになります。

1回目の更新直前の状態

目ID RowState
1 ~ 100 Unchanged
1000 Added

2回目の更新直前の状態

目ID RowState
1 ~ 100 Unchanged
1000 Added
1001 Added
→更新処理では目ID = 1000のレコードを新規追加しようとする。 →更新処理では目ID = 1000のレコードと目ID = 1001のレコードを新規追加しようとする。目ID = 1000のレコードは1回目の更新で追加されているので目IDの重複でエラーになる。

このケースで、なぜ2回目も目ID=1000のレコードを登録しようとするのかはRowStateを考えればすぐにわかります。目ID=1000のレコードのRowStateAddedだからです。1回目にデータベースに登録したからと言って自動的にRowStateAddedからUnchangedに変わることはありません。ですから自分でプログラムして登録が完了したレコードのRowStateUnchangedに戻す必要があります。

2回目の更新直前のあるべき状態

目ID RowState
1 ~ 100 Unchanged
1000 Unchanged
1001 Added
→1回目の更新が完了した時点で目ID = 1000のレコードのRowStateUnchangesになるべきである。

と言ってもRowStateプロパティは読み取り専用なので直接値を変更することはできません。RowStateUnchangedに戻すにはDataRowAcceptChangesメソッドを使用するか、DataTableAcceptChangesメソッドを使用します。DataRowAcceptChangesメソッドとはそのレコードのRowStateUnchangedに戻ります。DataTableAcceptChangesはそのDataTableに属するすべてのレコードのRowStateUnchangedに戻します。

たいていの場合すべてのレコードをUnchangedにするだけで用は足ります。上述の例では最後の方にあるTable.Disposeの1行つ上にTable.AcceptChangesと記述すればこの問題は回避できます。

失われた岩狸を求めて... - レコードの並び順は不定である

上の例をいろいろと試していると興味深い現象に遭遇する場合があります。たとえば、はじめは12番目に表示されている岩狸目ですが、いったんこれを削除してみます。

■画像2:岩狸目は12番目にある。Deleteキーを押してこれを削除する。

そして、一番下に新しく岩狸を登録しなおしましょう。

■画像3:岩狸目を最後の行に追加。

これでいったんプログラムを終了させて、もう一度プログラムを起動すると追加したはずの岩狸が12番目に表示されていないではないですか。一番下にもありません。どこへ行ったのかと探してみると変な所にまぎれています。

■画像4:再度プログラムを実行させると、岩狸目は奇天烈なところに…。

この現象は同じように操作しても発生しない場合もあります。

すべてのレコードは一見したところ目ID順にならんでいますが、実はそうではないということがこの現象からわかります。しかも目IDはこのテーブルの唯一のキーなのにその順番に並んでいないのです。このことはある程度データベースプログラムの経験がある人でも見落としている場合があります。

並び順を指定しなかった場合取得したレコードは予測不可能な順番で並んでいるということを覚えておいてください。

なお、並び順を指定する場合はSQL文でORDER BYを使用します。たとえば、次のとおりです。

SELECT * FROM T_目マスタ ORDER BY 目ID

SQL Serverの場合は並び順を指定しなかった場合はクラスタードインデックス(つまり主キー)の順に自動的に整列します。つまり、このあたりの仕様はデータベースによって異なります。そういったことをこまごまと気にするのは大変なので順番が重要な場合はORDER BYを指定する癖をつけておきましょう。

 

6.もっと楽に

 

これでDataTableの内容を自由自在にデータベースに反映できるようになりましたが、ちょっとコードの量が多いと感じられたのではないでしょうか。楽をする方法は色々とありますが、基本的には上記のコードはちゃんと理解していつでも自分で書けるくらいになっておかないとこの先苦労することになるかもしれません。

とは言え、楽をする方法も説明しておきましょう。その方法とはOleDbDataAdapterを使用することです。OleDbDataAdapterDataTableを取得する際に使用していますが、実は逆の流れ、つまりDataTableの内容をデータベースに書き込むときにも使用できるのです。

OleDbDataAdapterを使用すると、次のようにしてデータベースを更新することができます。

VB2005対応

Imports System.Data.OleDb

Public Class Form1

    Dim
Adapter As OleDbDataAdapter

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

       
'▼データ取得
   
    Dim Cn As New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Database\Animals.mdb")
        Dim SQLCm As OleDbCommand = Cn.CreateCommand
        Adapter =
New OleDbDataAdapter(SQLCm)
        Dim Builder As New OleDbCommandBuilder(Adapter)
        Dim Table As New DataTable

        SQLCm.CommandText =
"SELECT * FROM T_目マスタ"
       
Adapter.Fill(Table)

       
'▼値の表示
       
DataGridView1.DataSource = Table

       
'▼後処理
       
Table.Dispose()

    End
Sub
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Adapter.Update(DataGridView1.DataSource)

    End
Sub

End
Class

■リスト19:OleDbDataAdapterを利用して簡単にデータベースを更新する。

はじめのデータを取得するところが少し変わっていますが、データを更新するところがなんとたったの1行になっています。これは驚くべきことです。

これを見てOleDbDataAdapter.Updateを使えば簡単にデータベースを更新できるのかと思ったらそれはちょっと違います。

仕組みを簡単に説明しましょう。OleDbDataAdapterはデータを取得するときに使用するSQLをSelectCommandプロパティに記録していて、Fillメソッドが呼び出された時にはこのSQL文が動作します。同様にUpdateメソッドが発行された時には修正用のUpdateCommandプロパティ、新規追加用のInsertCommandプロパティ、削除用のDeleteCommandプロパティにあらかじめセットしておいたSQLが自動的に作動します。ここでいうSQLは文字列のSQL文ではなくOleDbCommandクラスのことです。

自分でループ処理をしなくても全レコードを処理してくれる点はありがたいのですが結局のところそれぞれのシーンに応じたSQLをあらかじめセットしておく必要があるのです。

しかし、上述のプログラムではどこでもそれらしいSQL文をセットしているところが見当たりません。OleDbDataAdapterクラスのコンストラクタに指定しているSQLCmSelectCommandの値となりますが、UpdateCommand等の指定にはなりません。これはどういうことかというと今回はSelectCommandに設定しているSQLが"SELECT * FROM T_目マスタ"というかなりシンプルなSQL文であるために、残りのSQLは自動生成可能なのです。

SQLを自動生成するのはOleDbCommandBuilderクラスで、このクラスはOleDbDataAdapterクラスと関連付けておくとあとは自動的にSQLを生成してくれます。関連付けているのはDim Builder As New OleDbCommandBuilder(Adapter)の部分で、これだけ書いておけばあとは余計なメソッドを呼び出したりプロパティを設定しなくても各SQL文が自動的に生成されます。

この自動生成機能が広範囲にわたっていれば結構楽ができるのですが、SelectCommandが一定以上に複雑な場合自動生成機能が利用できず、結局自分でSQL文を書くことになります。

それに実際にデータベースアプリケーションを作りだすと機械的に更新すればよいというわけではない場面にはたくさん出会うことになるでしょう。だからどのような場合にも対応できるように自分でループを回してSQL文を実行する方法を使いこなせるようにしておくべきなのです。