DBNull について

VB.NET とデータベースミドルウェアを組み合わせたアプリを作成する際、DB とのデータのやり取りが発生することと思います。この時に注意するのが DBNull というデータです。

目次

存在感を主張してくる、無いデータを考える

例えばこういうのです。

Module Module1

    Sub Main()

        ' DB からデータを取得する際、DBNull 値が含まれることがある(VB.NET の Nothing の DB 版)
        Dim dt As New DataTable
        dt.Columns.Add("Integer", GetType(Integer))
        dt.Columns.Add("String", GetType(String))
        dt.Columns.Add("Boolean", GetType(Boolean))
        dt.Rows.Add(New Object() {0, "taro", 25})
        dt.Rows.Add(New Object() {1, "jiro", 28})
        dt.Rows.Add(New Object() {2, "hanako", 32})
        dt.Rows.Add(New Object() {3, DBNull.Value, DBNull.Value})

        ' ※VB.NET の Nothing の DB 版ではあるが、Nothing と DBNull は別物
        If Nothing Is DBNull.Value Then
            Console.WriteLine("Nothing と DBNull.Value は、同じ")
        Else
            Console.WriteLine("Nothing と DBNull.Value は、違う")
        End If


        Console.ReadKey()
    End Sub

End Module

出力結果

Nothing と DBNull.Value は、違う

イメージ的には DBNull 値というのは、VB.NET における Nothing(null 状態)の DB 版なのですが、値自体は別々の存在になります。よって、上記のように比較しても不一致という結果になります。

で、DB からデータ取得して画面に表示しようとする際、DBNull 値があるとすんなりといきません。

Module Module1

    Sub Main()

        ' DB からデータを取得する際、DBNull 値が含まれることがある(VB.NET の Nothing の DB 版)
        Dim dt As New DataTable
        dt.Columns.Add("No", GetType(Integer))
        dt.Columns.Add("Name", GetType(String))
        dt.Columns.Add("Age", GetType(Integer))
        dt.Rows.Add(New Object() {0, "taro", 25})
        dt.Rows.Add(New Object() {1, "jiro", 28})
        dt.Rows.Add(New Object() {2, "hanako", 32})
        dt.Rows.Add(New Object() {3, DBNull.Value, DBNull.Value})

        Dim result As Object = dt.Rows(3)("Age")
        Dim age = CType(result, Integer)
        Console.WriteLine(age)


        Console.ReadKey()
    End Sub

End Module

出力結果

System.InvalidCastException: '型 'DBNull' から型 'Integer' への変換は無効です。'

なので、データが DBNull の可能性があるかどうかを事前にチェックする必要があるわけです。こういう風に。

Dim result As Object = dt.Rows(3)("Age")
If result IsNot DBNull.Value Then
    Dim age = CType(result, Integer)
    Console.WriteLine(age)
End If

例えば、Integer の値が DBNull だった時に、0 でいいのか空欄で扱いたいかは、アプリの要求仕様毎に分かれるわけなので、無条件で固定値に変換するわけにもいきません(無条件で固定値変換していいのなら、変換用メソッドを通して取得すればいいですよね)。

煩わしい反面、致し方なしでもあります。

null 許容型の値型の場合

Nullable(Of T) という "null 状態" も含めて扱える値型があります。あくまで値型ベースのままです。

Module Module1

    Sub Main()

        Dim i As Integer? = Nothing
        Dim b As Boolean? = Nothing

        ' 実体としては以下の書き方
        'Dim i2 As Nullable(Of Integer) = Nothing
        'Dim b2 As Nullable(Of Boolean) = Nothing

        Console.WriteLine(i.Value)


        Console.ReadKey()
    End Sub

End Module

出力結果

System.InvalidOperationException: 'Null 許容のオブジェクトには値を指定しなければなりません。'

Nothing をセットすると null 状態になります(値型ではあるが、Structure の規定値に変わるわけではない)。このままアクセスすると例外エラーが発生してしまいますので、アクセスする前に Nothing チェックするか任意の初期値をセットしておいて使います。

Module Module1

    Sub Main()

        Dim i As Integer? = 0
        Dim b As Boolean? = Nothing

        Console.WriteLine(i.Value)

        If b.HasValue Then
            Console.WriteLine(b.Value)
        Else
            b = False
            Console.WriteLine(b.Value)
        End If


        Console.ReadKey()
    End Sub

End Module

出力結果

0
False

この null 許容型の値型と、DataRowExtensions を組み合わせることで、かなり扱いやすくなります。Field メソッドに null 許容型の値型を指定することで、DBNull が入っていた場合、Nothing に自動変換して返却してくれます。

Dim age As Integer? = dt.Rows(3).Field(Of Integer?)("Age")
If age.HasValue Then
    Console.WriteLine(age.Value)
End If

こっちのほうが分かりやすいでしょうかね?

For Each row As DataRow In dt.Rows

    Dim age As Integer? = row.Field(Of Integer?)("Age")
    If age.HasValue Then
        Console.WriteLine($"Age: {age.Value}")
    Else
        Console.WriteLine($"Age: Nothing")
    End If

Next

出力結果

Age: 25
Age: 28
Age: 32
Age: Nothing

そして、さらに DBNull の場合に、その型のデフォルト値で構わない場合は、以下のように書くこともできます。大抵の場合は、皆さんこちらを使うで良いんじゃないでしょうか。もう DBNull も Nothing も考えなくてよくなります。

For Each row As DataRow In dt.Rows

    Dim age As Integer? = row.Field(Of Integer?)("Age")
    Console.WriteLine($"Age: {age.GetValueOrDefault}")

Next

出力結果

Age: 25
Age: 28
Age: 32
Age: 0

これにより、細かい制御を作りこみたいなら Nothing だけ考慮すればよく、Nothing すら考えたくない(考えなくてもいい仕様なのであれば)考えなくても良い記述が可能となります。