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 すら考えたくない(考えなくてもいい仕様なのであれば)考えなくても良い記述が可能となります。