【VB.NET極意】SqlCommandBuilderは使うな。SQL Serverへの堅牢なバルクインサートと「死なない」トランザクション設計
多くのエンジニアが「楽だから」という理由で `SqlCommandBuilder` を使い、その後の保守フェーズで地獄を見る。これがWindows Forms開発の現実だ。
今日は、業務自動化の最前線に立つ諸君へ、「泥臭いが最強に速く、かつ絶対に壊れない」SQL Server連携の極意を授ける。`SqlCommandBuilder` は自動生成の便利さに目がくらむが、型推論の曖昧さや意図しないSQL生成により、大規模データではパフォーマンスが崩壊する。
我々が目指すべきは、「明示的なトランザクション」と「再試行可能な接続設計」だ。
—
1. なぜ「自動生成」を捨て、「明示的なバルク処理」を選ぶのか
`SqlDataAdapter` と `SqlCommandBuilder` を組み合わせれば、グリッドから `Update()` 一発でデータは飛ぶ。だが、ネットワークが瞬断した瞬間、アプリはフリーズし、DBには中途半端なデータが残る。
実務で求められるのは、「成功か、完全な無か」の二択だ。これを担保するためには、`SqlTransaction` を制御し、`SqlBulkCopy` またはバッチ単位の `ExecuteNonQuery` を使い分ける設計が不可欠となる。
—
2. 堅牢なバルクインサートの実装パターン
今回は、最も汎用的かつ安全な「トランザクション付き一括登録」のコードを示す。ポイントは「接続の生存期間」を最小にすることだ。
Imports System.Data.SqlClient
Public Sub ExecuteBulkInsert(dataList As List(Of MyDataModel), connectionString As String)
‘ 接続のライフサイクルは最小限に。Usingステートメントは「神」である
Using conn As New SqlConnection(connectionString)
conn.Open()
‘ トランザクションを開始し、原子性を保証する
Using trans As SqlTransaction = conn.BeginTransaction()
Try
‘ 3回のリトライロジックを組み込む
Dim retryCount As Integer = 0
Dim maxRetry As Integer = 3
While retryCount < maxRetry
Try
Using cmd As New SqlCommand("INSERT INTO MyTable (Col1, Col2) VALUES (@c1, @c2)", conn, trans)
' パラメータの型指定は必須。暗黙の型変換はDBのインデックスを殺す
cmd.Parameters.Add("@c1", SqlDbType.NVarChar, 50)
cmd.Parameters.Add("@c2", SqlDbType.Int)
For Each item In dataList
cmd.Parameters("@c1").Value = item.Value1
cmd.Parameters("@c2").Value = item.Value2
cmd.ExecuteNonQuery()
Next
End Using
trans.Commit() ' 全て成功すれば確定
Exit While
Catch ex As SqlException When IsTransientError(ex)
' ネットワーク一時切断(Timeout等)のみリトライ
retryCount += 1
If retryCount >= maxRetry Then Throw
System.Threading.Thread.Sleep(1000 retryCount) ‘ 指数バックオフ
End Try
End While
Catch ex As Exception
‘ 何かあれば即ロールバック。これがデータ整合性の生命線
trans.Rollback()
Throw New Exception(“DB登録失敗。データ整合性は保護されました。”, ex)
End Try
End Using
End Using
End Sub
‘ 一時的なエラーか判断するヘルパー
Private Function IsTransientError(ex As SqlException) As Boolean
‘ エラーコード: 1205(デッドロック), -2(タイムアウト), 10054(接続切断)
Return ex.Number = 1205 OrElse ex.Number = -2 OrElse ex.Number = 10054
End Function
—
3. 実務で「バグを埋め込まない」ための鉄則
① SqlCommandBuilder への依存を断つ
`SqlCommandBuilder` は、テーブル構造が変わった瞬間に実行時エラーを引き起こすリスクがある。実務ツールであれば、SQLはコード内に定数として保持するか、ストアドプロシージャを呼び出すべきだ。設計をサボると、現場の誰かがテーブル定義を変えた瞬間に、君のツールはただのゴミ箱になる。
② UIスレッドをブロックしない
Windows Formsにおいて、DB処理をメインスレッドで走らせるのは「素人」の所業だ。`Async/Await` を使い、バックグラウンドで処理せよ。UIがフリーズしている間にユーザーがボタンを連打すれば、二重登録の悲劇が生まれる。
‘ UI側からの呼び出し例
Private Async Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
btnSave.Enabled = False ‘ 二重クリック防止
Try
Await Task.Run(Sub() ExecuteBulkInsert(myData, connStr))
MessageBox.Show(“完了しました”)
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
btnSave.Enabled = True
End Try
End Sub
③ ネットワークエラーは「想定内」
VPN環境や不安定なWi-Fi環境で業務アプリを動かすなら、エラーは「例外」ではなく「日常」だ。`IsTransientError` のような判定ロジックを必ず挟み、即座に死なせず、静かに再試行する。この「気遣い」が、君をただのプログラマーから「アーキテクト」へと昇華させる。
—
最後に:なぜVB.NETなのか
VB.NETは古いと言われる。しかし、Windowsの深いレイヤーへのアクセスと、Visual Studioによる開発生産性の高さは、今なお中小規模の業務システムにおいては最強の武器だ。
ツールを作って終わりではない。「誰もが直感的に使え、ネットワークが途切れても不機嫌にならない。そんな堅牢なシステムを、君の手で作り上げろ。」
コードを書くことは、ユーザーの業務を自動化するだけではない。彼らの「不安」をコードの整合性で消し去ることなのだ。さあ、IDEを開け。今日の設計は、昨日よりも強固なはずだ。
