VB.NETからSQL Server(Express 2017 など)へUPDATEを実行する際、フォームの日時入力が未入力だと「列はNULL許容なのにエラーになる」ケースがあります。原因は、VBのNothingと、データベースのNULLが同じ意味ではないためです。この記事では、Nothingを安全にNULLとして更新する方法(DBNull.Value)、WHERE句のパラメーター化、そしてAddWithValueの落とし穴まで、実務で困らない形で整理します。
現象:未入力の日時カラムがあるとUPDATEが失敗する
たとえば、VB側で次のようなUPDATEを組み立て、複数の日時カラムをパラメーターで更新しているとします。
UPDATE names2
SET trn_date1 = @trn_date1,
trn_date2 = @trn_date2,
...
trn_date9 = @trn_date9
WHERE account_no = @account_no;
フォーム側のdatTrn_Date9.EditValueが未入力のとき、VBでそのままパラメーターへ渡して実行すると、SQL Server側でエラーになったり、ADO.NET側で例外が発生したりします。一方でテーブル定義を見ると、該当列はNULLを許容しており、現在の値もNULLのまま…。「じゃあ未入力ならNULLで更新されればいいのに、なぜ?」というのが今回のポイントです。
原因:VBのNothingとDBのNULLは別物
まず押さえるべき結論はこれです。
- VBのNothing:.NETの世界で「参照がない(null)」を表す
- SQL ServerのNULL:DBの世界で「値が存在しない」を表す
- ADO.NET(SqlParameter)がSQLのNULLを表す値:
DBNull.Value
つまり、VBで「未入力だからNothing」のつもりで渡しても、SQL Serverが理解するNULLとして扱わせるにはDBNull.Valueへ寄せる必要があります。特にAddWithValueを使っている場合、値から型を推論する都合で「未入力が混ざった瞬間に推論が崩れる」ことが起きやすく、エラーとして表面化します。
| 概念 | 意味 | 主な登場場所 | UPDATEパラメーターとして渡すとき |
|---|---|---|---|
| Nothing | .NETのnull参照 | VB.NET / C# | 値・型の扱いがあいまいになりやすく、例外や暗黙変換の原因になりやすい |
| DBNull.Value | DBのNULLを表す特殊値 | ADO.NET | SQL ServerのNULLとして送信される(列がNULL許容ならNULL更新できる) |
| NULL | 値が存在しない | SQL Server | DB側の状態(比較は=ではなくIS NULLが必要) |
よくあるエラーと原因の切り分け早見表
発生するエラーメッセージはコードの書き方や列型で変わりますが、現場でよく遭遇するパターンを「原因→対策」で整理しておくと復旧が速くなります。
| 症状(例) | 起点になりやすい原因 | 対策の方向性 |
|---|---|---|
| datetimeへの変換に失敗する/範囲外の日付と言われる | 未入力がNothingではなくDateTime.MinValueや空文字で渡っている | UIの未入力表現をNULLへ統一(DBNull.Valueへ変換)、文字列はDateTimeへパース |
| パラメーターが指定されていない(未設定扱い) | パラメーター追加漏れ/名前不一致/条件分岐でAddしないパスがある | SQLに出てくる全パラメーターを必ずAddする(値はDBNull.Valueでも可) |
| データ型の不一致(暗黙変換)で遅い、または失敗 | AddWithValueの型推論が外れ、NVARCHARなどで送っている | SqlDbTypeを明示し、列型と揃える |
解決策:NothingはDBNull.Valueに置き換えて渡す
最小の修正は「未入力(Nothing)ならDBNull.Value、入力ありならその値」を渡すことです。質問の状況(EditValueが未入力だとNothingになる)なら、VBのIf演算子(Null合体)を使うと簡潔に書けます。
' 未入力(Nothing)なら DBNull.Value、入力ありなら EditValue をそのまま渡す
objCommand.Parameters.AddWithValue(
"@trn_date9",
If(CObj(datTrn_Date9.EditValue), DBNull.Value)
)
この書き方のポイントは、VBの2引数のIf(a, b)が「aがNothingならb、そうでなければa」を返す点です。EditValueはObject型で返ってくることが多く、CObjでObjectとして扱っておくと、Null合体が意図どおりに働きます。
より分かりやすさ重視なら、3引数の条件式で書いてもOKです(動作は同じです)。
Dim v = datTrn_Date9.EditValue
objCommand.Parameters.AddWithValue(
"@trn_date9",
If(v Is Nothing, DBNull.Value, v)
)
これでtrn_date9が直るなら、他の日時パラメーターも同様に「Nothing → DBNull.Value」へ置き換えるだけで、同種のエラーをまとめて潰せます。
実務向け:同じ処理を量産しないヘルパー関数
日時カラムが多いと、同じIf(... Is Nothing ...)がずらっと並びがちです。保守性を上げるなら、変換を関数化しておくと安全です。
Private Shared Function DbNullIfNothing(value As Object) As Object
Return If(value Is Nothing, DBNull.Value, value)
End Function
使う側は次のようにスッキリします。
objCommand.Parameters.AddWithValue("@trn_date1", DbNullIfNothing(datTrn_Date1.EditValue))
objCommand.Parameters.AddWithValue("@trn_date2", DbNullIfNothing(datTrn_Date2.EditValue))
objCommand.Parameters.AddWithValue("@trn_date9", DbNullIfNothing(datTrn_Date9.EditValue))
SQL Serverのdatetime範囲に注意(DateTime.MinValueが混ざると落ちる)
UIや変換処理によっては、未入力がNothingではなくDateTime.MinValue(0001/01/01)として入ってくることがあります。SQL Serverのdatetime型は扱える日付の範囲が狭いため、これをそのまま送ると「範囲外」エラーになりがちです。未入力として扱いたいなら、ここもNULLに寄せるのが安全です。
Private Shared Function DbNullIfEmptyDate(value As Object) As Object
If value Is Nothing OrElse value Is DBNull.Value Then Return DBNull.Value
' 文字列なら空をNULL扱い(TextBox由来のケース)
Dim s = TryCast(value, String)
If s IsNot Nothing AndAlso String.IsNullOrWhiteSpace(s) Then Return DBNull.Value
' DateTimeなら最小値をNULL扱い(未入力がこれになるUI対策)
If TypeOf value Is DateTime Then
Dim dt = CType(value, DateTime)
If dt = DateTime.MinValue Then Return DBNull.Value
End If
Return value
End Function
WHERE句の文字列連結はやめて、条件もパラメーター化する
UPDATEのSET句だけをパラメーター化していて、WHERE句が次のような文字列連結になっているコードはよく見かけます。
' これは避けたい(SQLインジェクション、型不一致、クォート漏れの温床)
Dim sql = "UPDATE names2 SET ... WHERE account_no = " & strAccount_No
結論として、WHERE句も必ずパラメーター化してください。理由は大きく3つあります。
- SQLインジェクション対策:文字列連結は入力混入の余地ができる
- 型不一致の回避:数値・文字列・日付のクォートや変換を人間が手で合わせなくて済む
- 実行計画の安定:パラメーター化はキャッシュが効きやすく、性能が安定しやすい
次のように@account_noを用意し、値も型も揃えるのが基本形です。
Dim sql As String =
"UPDATE names2 SET
trn_date1 = @trn_date1,
trn_date9 = @trn_date9
WHERE account_no = @account_no;"
Using conn As New SqlConnection(connectionString),
cmd As New SqlCommand(sql, conn)
' account_noの列型に合わせて Int / VarChar などを選ぶ
cmd.Parameters.AddWithValue("@account_no", strAccount_No)
cmd.Parameters.AddWithValue("@trn_date1", DbNullIfNothing(datTrn_Date1.EditValue))
cmd.Parameters.AddWithValue("@trn_date9", DbNullIfNothing(datTrn_Date9.EditValue))
conn.Open()
cmd.ExecuteNonQuery()
End Using
この時点で「Nothingが混ざると落ちる」「WHEREのクォートがずれて落ちる」「入力に特殊文字が入って壊れる」といった不具合が一気に減ります。
AddWithValueの落とし穴:型推論ミスで遅くなる・失敗する
ここからは、今回のエラーをきっかけに一緒に直しておきたい“実務的な改善”です。AddWithValueは手軽ですが、パラメーターの型を値から推論します。この推論が外れると、次のような問題が起きます。
| よくある状況 | AddWithValueで起きやすいこと | 困る理由 | 対策 |
|---|---|---|---|
| 日付を文字列で渡してしまう | NVARCHARとして送られ、SQL側でdatetimeへ暗黙変換 | 変換エラー、ロケール差、実行計画の悪化 | VB側でDateTimeにしてから渡す/SqlDbTypeを明示 |
| Nothingや空が混ざる | 型が決めきれず、例外や意図しない型になる | 今回のような更新失敗の原因 | DBNull.Valueへ変換し、できれば型も明示 |
| 文字列の長さが可変 | 推論でサイズが極端(例:NVARCHAR(4000)扱い) | インデックスが効きにくくなることがある | サイズ指定(Add(name, SqlDbType, size)) |
安定運用を狙うなら、次のようにSqlParameterで型(SqlDbType)を明示する方法がおすすめです。特に日時は、列定義がdatetimeかdatetime2かで選ぶ型が変わります。
' datetime列なら SqlDbType.DateTime
cmd.Parameters.Add("@trn_date9", SqlDbType.DateTime).Value =
DbNullIfNothing(datTrn_Date9.EditValue)
' datetime2列なら SqlDbType.DateTime2
cmd.Parameters.Add("@trn_date9", SqlDbType.DateTime2).Value =
DbNullIfNothing(datTrn_Date9.EditValue)
SQL Serverの日時型とSqlDbTypeの対応
列定義に合わせて型を揃えると、暗黙変換による性能低下や変換エラーを避けやすくなります。
| SQL Serverの列型 | おすすめのSqlDbType | 補足 |
|---|---|---|
| date | SqlDbType.Date | 日付だけを持つ。時刻が不要ならこれが最も安全 |
| datetime | SqlDbType.DateTime | 互換性は高いが範囲が狭い。MinValue混入に注意 |
| datetime2 | SqlDbType.DateTime2 | 範囲・精度が広い。新規設計ならこちらが無難 |
| smalldatetime | SqlDbType.SmallDateTime | 分単位。範囲も狭いので業務要件を要確認 |
型を明示しておけば、値がNULL(DBNull.Value)でも「このパラメーターは日時」という情報が保持されるため、推論ミスを避けられます。
UIコントロール別:未入力の“表現”は統一されない
今回のEditValueのように、UIコントロールが未入力をどう表現するかはまちまちです。現場で混在しやすい代表例を整理しておきます。
| 入力元の例 | 未入力時に返りがちな値 | そのまま渡すと起きがちな問題 | おすすめの扱い |
|---|---|---|---|
| DevExpress DateEdit(EditValue) | Nothing / DBNull.Value / DateTime.MinValue(設定次第) | 型推論ミス、範囲外日付でSQL変換失敗 | Nothing/DBNull/MinValueをDBNull.Valueへ |
| DateTimePicker | 常にDateTime(未入力という概念がない) | 「未入力」のつもりが勝手に今日の日付になる等 | チェックボックス連動などでNULLを表現 |
| TextBox | 空文字(””) | 文字列→datetime変換でエラー | 空文字はDBNull.Value、入力ありはDateTimeへパース |
Nullable(Of DateTime)を使うと“意図”がコードに残る
フォーム値をそのままObjectとして扱うと、「本当に日付なのか」「未入力を許すのか」がコードから読み取りにくくなります。可能なら、更新直前でDateTime?(VBではNullable(Of DateTime))に落としておくと、意図が明確になります。
Dim dt9 As DateTime? = Nothing
If datTrn_Date9.EditValue IsNot Nothing Then
' EditValueがDateTimeで来る前提なら安全にキャスト
dt9 = CType(datTrn_Date9.EditValue, DateTime)
End If
cmd.Parameters.Add("@trn_date9", SqlDbType.DateTime).Value =
If(dt9.HasValue, CType(dt9.Value, Object), DBNull.Value)
この形にすると、「dt9が値を持つときだけ更新値になる」「持たないときはNULLになる」が一目で分かります。
NULLで“上書き”したいのか、“更新しない”にしたいのかを決める
ここは業務要件で分かれる落とし穴です。
- 未入力ならNULLに上書きしたい:今回のようにDBNull.Valueを渡せばOK
- 未入力なら既存値を保持したい(更新しない):SQLの書き方を変える必要がある
後者(更新しない)の代表的な書き方は、COALESCE(またはISNULL)で「パラメーターがNULLなら列の現値」を採用する方法です。
UPDATE names2
SET trn_date9 = COALESCE(@trn_date9, trn_date9)
WHERE account_no = @account_no;
ただしこの書き方にはトレードオフがあります。NULLを“明示的にセットしたい”要求が後から出ると困ります(NULLを渡しても現値保持になってしまう)。要件が揺れそうなら、「更新する/しない」を別フラグで持つ、または更新対象の列だけ動的にSET句へ追加する、といった設計も検討してください。
読み取り側も注意:NULLはIsDBNullで判定する
NULL更新を導入すると、次は「読み取ったときに落ちる」パターンも増えます。DataReaderやDataTableで値を取り出すと、DBのNULLはDBNull.Valueとして返ってきます。DateTimeへ直キャストすると例外になるため、読み取り側もセットで整備しておくのがおすすめです。
Using reader = cmd.ExecuteReader()
If reader.Read() Then
Dim dt9 As DateTime? = Nothing
If Not reader.IsDBNull(reader.GetOrdinal("trn_date9")) Then
dt9 = reader.GetDateTime(reader.GetOrdinal("trn_date9"))
End If
' dt9.HasValue で存在チェックできる
End If
End Using
サンプル:日時列が多いUPDATEを安全に実装する
最後に、日時列が多数あるテーブルに対して、未入力をNULLとして更新しつつ、型も明示して安定させるサンプルを載せます。列数が多い場合は、共通化したメソッドで追加するだけで、同じミスを繰り返さずに済みます。
Imports System.Data
Imports System.Data.SqlClient
Public Sub UpdateNames2Dates(connectionString As String, accountNo As Integer)
Dim sql As String =
"UPDATE names2 SET
trn_date1 = @trn_date1,
trn_date2 = @trn_date2,
trn_date3 = @trn_date3,
trn_date9 = @trn_date9
WHERE account_no = @account_no;"
Using conn As New SqlConnection(connectionString),
cmd As New SqlCommand(sql, conn)
' WHERE句も必ずパラメーター化(列型に合わせてInt/VarCharを選ぶ)
cmd.Parameters.Add("@account_no", SqlDbType.Int).Value = accountNo
' 日時パラメーターは型を明示し、未入力はDBNullへ寄せる
AddDateParam(cmd, "@trn_date1", datTrn_Date1.EditValue)
AddDateParam(cmd, "@trn_date2", datTrn_Date2.EditValue)
AddDateParam(cmd, "@trn_date3", datTrn_Date3.EditValue)
AddDateParam(cmd, "@trn_date9", datTrn_Date9.EditValue)
conn.Open()
cmd.ExecuteNonQuery()
End Using
End Sub
Private Sub AddDateParam(cmd As SqlCommand, name As String, editValue As Object)
Dim p = cmd.Parameters.Add(name, SqlDbType.DateTime) ' 列がdatetime2ならDateTime2へ
p.Value = DbNullIfEmptyDate(editValue)
End Sub
Private Function DbNullIfEmptyDate(value As Object) As Object
If value Is Nothing OrElse value Is DBNull.Value Then Return DBNull.Value
Dim s = TryCast(value, String)
If s IsNot Nothing AndAlso String.IsNullOrWhiteSpace(s) Then Return DBNull.Value
If TypeOf value Is DateTime Then
Dim dt = CType(value, DateTime)
If dt = DateTime.MinValue Then Return DBNull.Value
End If
Return value
End Function
この形にしておくと、次のメリットが得られます。
- 未入力は確実にNULL(DBNull.Value)としてDBへ渡せる
- 日時パラメーターの型が固定され、暗黙変換や推論ミスのリスクが下がる
- WHERE句も含めて完全にパラメーター化され、保守性と安全性が上がる
まとめ:NULL更新の基本は「DBNull.Value」と「パラメーター化」
SQL Serverの列がNULL許容であっても、VB側でNothingをそのまま渡すだけでは安定してNULL更新できません。未入力の可能性がある値は、DBのNULLを表すDBNull.Valueへ変換してから渡すのが基本です。あわせて、WHERE句の文字列連結をやめて条件もパラメーター化し、可能ならAddWithValueを卒業して型を明示すると、エラーと性能問題の両方を予防できます。

コメント