【C#】「型 ‘System.DBNull’ のオブジェクトを型 ‘~’ にキャストできません」の解決【現場の備忘録】

プログラム

DBやDataTableから値を取ってきて変数に入れたら、「型 ‘System.DBNull’ のオブジェクトを型 ‘System.String’ にキャストできません。」で落ちた。よくあるやつ。

原因はシンプルなので、対処のパターンを覚えておけば怖くない。

原因:DBのNULLは「null」じゃない

DB(やDataTable)のNULLは、C#のnullではなくDBNull.Value という専用の値で入っている。だから(string)dr["COL"](int)dr["COL"]のように直接キャストすると、NULLの列で「DBNullはstringにできません」と怒られる。

エラーの再現
using System;
using System.Data;

class Program
{
    static void Main()
    {
        DataTable dt = new DataTable();
        dt.Columns.Add("NAME", typeof(string));

        // NULL(DBNull.Value)を含む行を追加
        dt.Rows.Add(DBNull.Value);

        DataRow dr = dt.Rows[0];

        // ↓ここで例外:型 'System.DBNull' のオブジェクトを型 'System.String' にキャストできません
        string name = (string)dr["NAME"];

        Console.WriteLine(name);
    }
}

これで確実に例外が再現できる。実際のDBでも、NULLが入っている列を同じように直接キャストすると同じことが起きる。

System.InvalidCastException: 「『System.DBNull』 型のオブジェクトを 『System.String』 型にキャストできません。」
System.InvalidCastException: 「『System.DBNull』 型のオブジェクトを 『System.String』 型にキャストできません。」

解決方法

対処は大きく3つ。

1. DBNullかどうかを判定してから取り出す。いちばん確実。

// 参照型(string など)
string name = dr["NAME"] == DBNull.Value ? "" : (string)dr["NAME"];

// 値型(int など)
int age = dr["AGE"] == DBNull.Value ? 0 : (int)dr["AGE"];

// DataRow なら IsNull でも判定できる
if (!dr.IsNull("NAME"))
{
    string n = (string)dr["NAME"];
}

2. 文字列だけは Convert.ToString が使えるConvert.ToStringDBNullを渡しても例外にならず空文字(””)を返すので、文字列の取得には手軽。

string name = Convert.ToString(dr["NAME"]); // NULLなら ""(空文字)が返る

ただし注意Convert.ToInt32Convert.ToDecimalConvert.ToDateTimeは、DBNullを渡すと例外(InvalidCastException)になる。数値や日付は「Convertを通せば安全」ではないので、その場合は1のDBNull判定か、次の3の方法を使う。

3. 取得用のヘルパー(拡張メソッド)にまとめる。毎回1や2を書くのは面倒なので、DBNullの吸収をメソッド1つに閉じ込めておくのが現場では一番ラク。これが次の項目。

3. コピペで使える取得ヘルパー

毎回書くのは面倒なので、DataRowからDBNullを吸収して値を取る拡張メソッドを用意しておくと楽。

using System;
using System.Data;

public static class DataRowExtensions
{
    /// <summary>
    /// DataRowから値を取得する。NULL(DBNull)の場合は既定値を返す
    /// </summary>
    public static T GetValue<T>(this DataRow row, string columnName, T defaultValue = default!)
    {
        object value = row[columnName];

        // NULL または DBNull なら既定値を返す
        if (value == null || value == DBNull.Value) return defaultValue;

        // 目的の型に変換して返す
        return (T)Convert.ChangeType(value, typeof(T));
    }
}
使い方
// NULLでも例外にならず、指定した既定値が返る
string name = dr.GetValue<string>("NAME", "");
int     age = dr.GetValue<int>("AGE", 0);
decimal amt = dr.GetValue<decimal>("AMOUNT", 0m);

冒頭のエラーの再現と組み合わせるとこのようになる。

using System;
using System.Data;

class Program
{
    static void Main()
    {
        DataTable dt = new DataTable();
        dt.Columns.Add("NAME", typeof(string));

        // NULL(DBNull.Value)を含む行を追加
        dt.Rows.Add(DBNull.Value);

        DataRow dr = dt.Rows[0];

        // ↓ここで例外:型 'System.DBNull' のオブジェクトを型 'System.String' にキャストできません 
        //string name = (string)dr["NAME"];
        string name = dr.GetValue<string>("NAME", "");

        Console.WriteLine(name);
    }
}
public static class DataRowExtensions
{
    /// <summary>
    /// DataRowから値を取得する。NULL(DBNull)の場合は既定値を返す
    /// </summary>
    public static T GetValue<T>(this DataRow row, string columnName, T defaultValue = default!)
    {
        object value = row[columnName];

        // NULL または DBNull なら既定値を返す
        if (value == null || value == DBNull.Value) return defaultValue;

        // 目的の型に変換して返す
        return (T)Convert.ChangeType(value, typeof(T));
    }
}

実行するとエラーとなっていた部分が空白で返され正常に処理が通ったことが分かる。

string name = dr.GetValue<string>(“NAME”, “”);で「””」が返されている

これで呼び出し側はDBNullを気にせず、欲しい型でそのまま受け取れる。

さいごに

DBを触るとほぼ必ず出会うエラーなので、「DBのNULL=DBNull.Value」とだけ覚えておけば原因はすぐ分かる。

拡張メソッドを1つ用意しておくと、以降のDataTable→値取得まわりがぐっと楽になるのでおすすめ。

コメント

タイトルとURLをコピーしました