[.net] DBNull을 확인한 다음 변수에 할당하는 가장 효율적인 방법은 무엇입니까?


Answers

나는 뭔가를 놓치고 있어야합니다. DBNull 정확히 검사하지 않고 DataRow.IsNull 메서드가 수행하는 작업은 무엇입니까?

나는 다음 두 가지 확장 방법을 사용 해왔다.

public static T? GetValue<T>(this DataRow row, string columnName) where T : struct
{
    if (row.IsNull(columnName))
        return null;

    return row[columnName] as T?;
}

public static string GetText(this DataRow row, string columnName)
{
    if (row.IsNull(columnName))
        return string.Empty;

    return row[columnName] as string ?? string.Empty;
}

용법:

int? id = row.GetValue<int>("Id");
string name = row.GetText("Name");
double? price = row.GetValue<double>("Price");

Nullable<T>GetValue<T> 대한 값을 반환하지 않게하려면 default(T) 또는 다른 옵션을 쉽게 반환 할 수 있습니다.

관련없는 메모에서 Stevo3000의 제안에 대한 VB.NET 대안이 있습니다.

oSomeObject.IntMember = If(TryConvert(Of Integer)(oRow("Value")), iDefault)
oSomeObject.StringMember = If(TryCast(oRow("Name"), String), sDefault)

Function TryConvert(Of T As Structure)(ByVal obj As Object) As T?
    If TypeOf obj Is T Then
        Return New T?(DirectCast(obj, T))
    Else
        Return Nothing
    End If
End Function
Question

이 질문은 가끔씩 나오지만 만족스러운 답을 찾지 못했습니다.

일반적인 패턴은 다음과 같습니다 (행은 DataRow 임).

 if (row["value"] != DBNull.Value)
 {
      someObject.Member = row["value"];
 }

내 첫 번째 질문은 더 효율적입니다 (조건을 뒤집어 버렸습니다) :

  row["value"] == DBNull.Value; // Or
  row["value"] is DBNull; // Or
  row["value"].GetType() == typeof(DBNull) // Or... any suggestions?

This .GetType ()이 빠르다는 것을 나타내지 만, 컴파일러는 내가하지 못하는 몇 가지 트릭을 알고있을 것입니다.

두 번째 질문은 row [ "value"]의 값을 캐싱 할 가치가 있습니까? 아니면 컴파일러가 인덱서를 멀리 최적화합니까?

예 :

  object valueHolder;
  if (DBNull.Value == (valueHolder = row["value"])) {}

노트:

  1. 행 [ "value"]이 (가) 있습니다.
  2. 컬럼의 컬럼 인덱스를 알지 못합니다 (따라서 컬럼 이름 조회).
  3. 특히 DBNull을 확인한 다음 과제 (조숙 한 최적화 등이 아닌)에 대해 구체적으로 묻습니다.

몇 가지 시나리오 (초 단위의 시간, 10,000,000 번의 시도)를 벤치마킹했습니다.

row["value"] == DBNull.Value: 00:00:01.5478995
row["value"] is DBNull: 00:00:01.6306578
row["value"].GetType() == typeof(DBNull): 00:00:02.0138757

Object.ReferenceEquals의 성능은 "=="과 같습니다.

가장 흥미로운 결과? 대 / 소문자 별 (예 : "값"대신 "값") 열의 이름이 불일치하면 문자열에 대해 약 10 배 더 오래 걸립니다.

row["Value"] == DBNull.Value: 00:00:12.2792374

이야기의 도덕은 인덱스로 열을 검색 할 수없는 경우 인덱서에 공급하는 열 이름이 DataColumn의 이름과 정확하게 일치하는지 확인하는 것입니다.

값을 캐싱하는 것이 거의 두 배 빠른 것처럼 보입니다.

No Caching: 00:00:03.0996622
With Caching: 00:00:01.5659920

따라서 가장 효율적인 방법 은 다음과 같습니다 .

 object temp;
 string variable;
 if (DBNull.Value != (temp = row["value"]))
 {
      variable = temp.ToString();
 }



나는 확장 메소드와 비슷한 것을했다. 내 코드는 다음과 같습니다.

public static class DataExtensions
{
    /// <summary>
    /// Gets the value.
    /// </summary>
    /// <typeparam name="T">The type of the data stored in the record</typeparam>
    /// <param name="record">The record.</param>
    /// <param name="columnName">Name of the column.</param>
    /// <returns></returns>
    public static T GetColumnValue<T>(this IDataRecord record, string columnName)
    {
        return GetColumnValue<T>(record, columnName, default(T));
    }

    /// <summary>
    /// Gets the value.
    /// </summary>
    /// <typeparam name="T">The type of the data stored in the record</typeparam>
    /// <param name="record">The record.</param>
    /// <param name="columnName">Name of the column.</param>
    /// <param name="defaultValue">The value to return if the column contains a <value>DBNull.Value</value> value.</param>
    /// <returns></returns>
    public static T GetColumnValue<T>(this IDataRecord record, string columnName, T defaultValue)
    {
        object value = record[columnName];
        if (value == null || value == DBNull.Value)
        {
            return defaultValue;
        }
        else
        {
            return (T)value;
        }
    }
}

그것을 사용하려면, 당신은 뭔가를 할 것입니다.

int number = record.GetColumnValue<int>("Number",0)



C #에서 다음 코드를 사용합니다 ( VB.NET 은 단순하지 않습니다).

이 코드는 null / DBNull이 아닌 경우 값을 할당하고, 그렇지 않으면 컴파일러가 할당을 무시할 수 있도록 LHS 값으로 설정할 수있는 기본값을 지정합니다.

oSomeObject.IntMemeber = oRow["Value"] as int? ?? iDefault;
oSomeObject.StringMember = oRow["Name"] as string ?? sDefault;



이것이 내가 DataRow에서 읽는 것을 처리하는 방법이다.

///<summary>
/// Handles operations for Enumerations
///</summary>
public static class DataRowUserExtensions
{
    /// <summary>
    /// Gets the specified data row.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="dataRow">The data row.</param>
    /// <param name="key">The key.</param>
    /// <returns></returns>
    public static T Get<T>(this DataRow dataRow, string key)
    {
        return (T) ChangeTypeTo<T>(dataRow[key]);
    }

    private static object ChangeTypeTo<T>(this object value)
    {
        Type underlyingType = typeof (T);
        if (underlyingType == null)
            throw new ArgumentNullException("value");

        if (underlyingType.IsGenericType && underlyingType.GetGenericTypeDefinition().Equals(typeof (Nullable<>)))
        {
            if (value == null)
                return null;
            var converter = new NullableConverter(underlyingType);
            underlyingType = converter.UnderlyingType;
        }

        // Try changing to Guid  
        if (underlyingType == typeof (Guid))
        {
            try
            {
                return new Guid(value.ToString());
            }
            catch

            {
                return null;
            }
        }
        return Convert.ChangeType(value, underlyingType);
    }
}

사용 예 :

if (dbRow.Get<int>("Type") == 1)
{
    newNode = new TreeViewNode
                  {
                      ToolTip = dbRow.Get<string>("Name"),
                      Text = (dbRow.Get<string>("Name").Length > 25 ? dbRow.Get<string>("Name").Substring(0, 25) + "..." : dbRow.Get<string>("Name")),
                      ImageUrl = "file.gif",
                      ID = dbRow.Get<string>("ReportPath"),
                      Value = dbRow.Get<string>("ReportDescription").Replace("'", "\'"),
                      NavigateUrl = ("?ReportType=" + dbRow.Get<string>("ReportPath"))
                  };
}

소품을 괴물 에게 내 .Net ChageTypeTo 코드가 있습니다.




개인적으로 IDataRecord에 의해 노출 된 명시 적 IsDbNull 메서드를 사용하고 중복 된 문자열 조회를 피하기 위해 열 인덱스를 캐시하는이 구문을 선호합니다.

가독성을 높이기 위해 다음과 같이 확장되었습니다.

int columnIndex = row.GetOrdinal("Foo");
string foo; // the variable we're assigning based on the column value.
if (row.IsDBNull(columnIndex)) {
  foo = String.Empty; // or whatever
} else { 
  foo = row.GetString(columnIndex);
}

DAL 코드에서 단일 라인에 맞게 다시 작성 -이 예제에서 row["Bar"] 가 null 인 경우 int bar = -1 지정합니다.

int i; // can be reused for every field.
string foo  = (row.IsDBNull(i  = row.GetOrdinal("Foo")) ? null : row.GetString(i));
int bar = (row.IsDbNull(i = row.GetOrdinal("Bar")) ? -1 : row.GetInt32(i));

인라인 할당은 모르는 경우 혼란 스러울 수 있지만 전체 작업을 한 줄로 유지하므로 한 블록의 코드에서 여러 열의 속성을 채울 때 가독성이 향상됩니다.




데이터베이스에서 많은 양의 데이터를 읽는 프로그램에서 IsDBNull이 있습니다. IsDBNull을 사용하면 약 20 초 내에 데이터를로드합니다. IsDBNull이 없으면 약 1 초.

그래서 나는 그것을 사용하는 것이 더 좋다고 생각한다.

public String TryGetString(SqlDataReader sqlReader, int row)
{
    String res = "";
    try
    {
        res = sqlReader.GetString(row);
    }
    catch (Exception)
    { 
    }
    return res;
}



객체가 문자열 일 수있는 까다로운 경우가 있습니다. 아래의 확장 메소드 코드는 모든 경우를 처리합니다. 사용 방법은 다음과 같습니다.

    static void Main(string[] args)
    {
        object number = DBNull.Value;

        int newNumber = number.SafeDBNull<int>();

        Console.WriteLine(newNumber);
    }



    public static T SafeDBNull<T>(this object value, T defaultValue) 
    {
        if (value == null)
            return default(T);

        if (value is string)
            return (T) Convert.ChangeType(value, typeof(T));

        return (value == DBNull.Value) ? defaultValue : (T)value;
    } 

    public static T SafeDBNull<T>(this object value) 
    { 
        return value.SafeDBNull(default(T)); 
    } 



컴파일러는 인덱서를 최적화하지 않습니다 (예 : 행 [ "value"]를 두 번 사용하는 경우). 그래, 약간 더 빠르게 할 수 있습니다.

object value = row["value"];

값을 두 번 사용하십시오. .GetType ()을 사용하면 null 인 경우 문제가 발생합니다 ...

DBNull.Value 는 실제로 싱글 톤이므로, 네 번째 옵션을 추가하기 위해 아마도 ReferenceEquals를 사용할 수 있습니다. 그러나 실제로는 여기에 너무 걱정한다고 생각합니다 ... "사이에"다른 속도가 있다고 생각하지 않습니다. "=="등이 성능 문제의 원인이 될 수 있습니다. 전체 코드를 프로파일 링 하고 중요한 사항에 집중하십시오.




DataRow의 행 [ "fieldname"]이 DbNull 인 경우 0으로 바꾸고 그렇지 않으면 10 진수 값을 얻습니다.

decimal result = rw["fieldname"] as decimal? ?? 0;



public static class DBH
{
    /// <summary>
    /// Return default(T) if supplied with DBNull.Value
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <returns></returns>
    public static T Get<T>(object value)
    {   
        return value == DBNull.Value ? default(T) : (T)value;
    }
}

이런 식으로 사용하십시오.

DBH.Get<String>(itemRow["MyField"])



다음 방법을 사용해야합니다.

Convert.IsDBNull()

프레임 워크에 내장되어 있다는 점을 감안할 때, 이것이 가장 효율적이라고 생각합니다.

나는 다음과 같은 것을 제안 할 것이다.

int? myValue = (Convert.IsDBNull(row["column"]) ? null : (int?) Convert.ToInt32(row["column"]));

그리고 네, 컴파일러가 캐시해야합니다.




가능한 한이 검사를 피하려고합니다.

null 보관할 수없는 열에 대해서는 수행 할 필요가 없습니다.

Nullable 값 유형 ( int? 등)에 저장하는 경우 int? 로 변환하면 as int? .

string.Emptynull 을 구별 할 필요가없는 경우 DBNull이 string.Empty 를 반환하므로 .ToString() 호출 할 수 있습니다.




나는 항상 다음을 사용한다.

if (row["value"] != DBNull.Value)
  someObject.Member = row["value"];

짧고 포괄적 인 것으로 밝혀졌습니다.




나는 여기에 거의 접근하지 못하여 전망 OP가 가장 걱정스럽지 않다. (Marc Gravell, Stevo3000, Richard Szalay, Neil, Darren Koppand) 대부분은 불필요하게 복잡하다. 이것이 쓸모없는 마이크로 최적화라는 것을 완전히 인식하고 있기 때문에 기본적으로 다음을 사용해야한다고 말하게하십시오.

1) DataReader / DataRow에서 값을 두 번 읽지 마십시오. 따라서 null 검사와 캐스트 / 변환 전에 캐싱하거나 record[X] 개체를 적절한 서명이있는 사용자 지정 확장 메서드에 직접 전달하는 것이 좋습니다.

2) 위의 내용을 적용하려면 DataReader / DataRow에서 내부적으로 record[X] 를 호출하므로 IsDBNull 함수를 사용하지 마십시오. 따라서 실제로 두 번 수행하게됩니다.

3) 타입 비교는 일반적으로 값 비교보다 느립니다. record[X] == DBNull.Value 더 잘 record[X] == DBNull.Value .

4) Convert 클래스를 Convert 클래스로 호출하는 것보다 직접 형 변환이 빠르다.

5) 마지막으로 열 이름 대신 색인으로 레코드에 액세스하는 것이 빠릅니다.

나는 Szalay, Neil과 Darren Koppand의 접근 방식이 나아질 것이라고 생각한다. 나는 IDataRecord ( IDataReader 로 더 좁히고 싶지만)와 index / column name을 취하는 Darren Koppand의 확장 메소드 접근법을 특히 좋아한다.

그것을 부르세요.

record.GetColumnValue<int?>("field");

하지

record.GetColumnValue<int>("field");

당신이 0DBNull 을 구별 할 필요가있는 경우에 대비해서. 예를 들어 열거 형 필드에 null 값이있는 경우 default(MyEnum) 이 반환되는 첫 번째 열거 형 값을 위험에 default(MyEnum) . 그래서 더 나은 기록 record.GetColumnValue<MyEnum?>("Field") .

DataRow 에서 읽으 DRYing 일반 코드를 DRYing 하여 DataRowIDataReader 에 대한 확장 메서드를 만듭니다.

public static T Get<T>(this DataRow dr, int index, T defaultValue = default(T))
{
    return dr[index].Get<T>(defaultValue);
}

static T Get<T>(this object obj, T defaultValue) //Private method on object.. just to use internally.
{
    if (obj.IsNull())
        return defaultValue;

    return (T)obj;
}

public static bool IsNull<T>(this T obj) where T : class 
{
    return (object)obj == null || obj == DBNull.Value;
} 

public static T Get<T>(this IDataReader dr, int index, T defaultValue = default(T))
{
    return dr[index].Get<T>(defaultValue);
}

이제 다음과 같이 호출하십시오.

record.Get<int>(1); //if DBNull should be treated as 0
record.Get<int?>(1); //if DBNull should be treated as null
record.Get<int>(1, -1); //if DBNull should be treated as a custom value, say -1

이것이 첫 번째 장소에서 ( record.GetInt32 , record.GetString 메서드 대신) 프레임 워크에 있어야하는 방법이라고 생각합니다. 런타임 예외가 없으며 null 값을 처리 할 수있는 유연성을 제공합니다.

필자의 경험에 비추어 볼 때 데이터베이스에서 읽는 일반적인 방법은별로 없습니다. 나는 항상 다양한 타입의 커스텀을 처리해야했기 때문에, 내 자신의 GetInt , GetEnum , GetGuid 등의 메소드를 장기적으로 작성해야했습니다. 기본적으로 db에서 문자열을 읽을 때 공백을 DBNull 거나 DBNull 을 빈 문자열로 처리하려면 어떻게해야합니까? 또는 십진수가 모든 후행 0을 절단해야합니다. 다른 커넥터 드라이버가 기본 데이터베이스가 문자열 또는 바이너리로 저장할 수있을 때 다르게 동작하는 Guid 유형의 경우 가장 문제가있었습니다. 나는 이런 과부하가있다 :

static T Get<T>(this object obj, T defaultValue, Func<object, T> converter)
{
    if (obj.IsNull())
        return defaultValue;

    return converter  == null ? (T)obj : converter(obj);
}

Stevo3000의 접근 방식을 사용하면 좀 부끄럽고 지루한 호출을 발견하고 일반 함수를 만드는 것이 더 어려울 것입니다.




이 작업을 수행 한 것은 아니지만 이중 인덱서 호출을 처리하고 정적 / 확장 메서드를 사용하여 코드를 깨끗하게 유지할 수 있습니다.

예.

public static IsDBNull<T>(this object value, T default)
{
    return (value == DBNull.Value)
        ? default
        : (T)value;
}

public static IsDBNull<T>(this object value)
{
    return value.IsDBNull(default(T));
}

그때:

IDataRecord record; // Comes from somewhere

entity.StringProperty = record["StringProperty"].IsDBNull<string>(null);
entity.Int32Property = record["Int32Property"].IsDBNull<int>(50);

entity.NoDefaultString = record["NoDefaultString"].IsDBNull<string>();
entity.NoDefaultInt = record["NoDefaultInt"].IsDBNull<int>();

또한 한 곳에서 null 검사 논리를 유지하는 이점이 있습니다. 단점은 물론 이것이 추가 메서드 호출이라는 것입니다.

그냥 생각.