VB.NETでSQL ServerのUPDATEにNothingを渡してエラーになる原因と解決策|DBNull.ValueでNULL更新・パラメーター化

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.ValueDBのNULLを表す特殊値ADO.NETSQL ServerのNULLとして送信される(列がNULL許容ならNULL更新できる)
NULL値が存在しないSQL ServerDB側の状態(比較は=ではなく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補足
dateSqlDbType.Date日付だけを持つ。時刻が不要ならこれが最も安全
datetimeSqlDbType.DateTime互換性は高いが範囲が狭い。MinValue混入に注意
datetime2SqlDbType.DateTime2範囲・精度が広い。新規設計ならこちらが無難
smalldatetimeSqlDbType.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を卒業して型を明示すると、エラーと性能問題の両方を予防できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次