Controlar valores Null

Se utiliza un valor NULL en una base de datos relacional cuando el valor de una columna es desconocido o falta. Un valor NULL no es una cadena vacía (para tipos de datos de caracteres o de fecha y hora) ni un valor cero (para tipos de datos numéricos). La especificación ANSI SQL-92 indica que un valor NULL debe ser el mismo para todos los tipos de datos, de modo que todos los valores NULL se traten de manera uniforme. El espacio de nombres System.Data.SqlTypes proporciona la semántica de NULL implementando la interfaz INullable. Cada uno de los tipos de datos de System.Data.SqlTypes tiene su propia propiedad IsNull y un valor Null que se puede asignar a una instancia de ese tipo de datos.

Nota:

En la versión 2.0 de .NET Framework se introduce la compatibilidad con tipos de valor que aceptan valores NULL, lo que permite a los programadores ampliar un tipo de valor para representar todos los valores del tipo subyacente. Estos tipos de valor CLR que aceptan valores NULL representan una instancia de la estructura Nullable. Esta funcionalidad es especialmente útil cuando se aplica la conversión boxing y la conversión unboxing de tipos de valor, lo que proporciona compatibilidad mejorada con los tipos de objeto. Los tipos de valor CLR que aceptan valores NULL no están pensados para el almacenamiento de valores NULL de base de datos, porque un valor NULL ANSI SQL no se comporta del mismo modo que una referencia null (o Nothing, en Visual Basic). Para trabajar con valores NULL de ANSI SQL de la base de datos, utilice valores NULL de tipo System.Data.SqlTypes en lugar de Nullable. Para más información sobre el trabajo con tipos de valor CLR que aceptan valores NULL en Visual Basic, consulte Tipos de valor que aceptan valores NULL y, para C#, consulte Tipos de valor que aceptan valores NULL.

Valores NULL y la lógica de tres valores

Al permitir valores NULL en definiciones de columna, se introduce la lógica de tres valores en la aplicación. Una comparación puede evaluarse en una de estas tres condiciones:

  • True

  • False

  • Desconocido

Dado que se considera que NULL es desconocido, dos valores NULL comparados entre sí no se consideran iguales. En las expresiones que usan operadores aritméticos, si alguno de los operandos es NULL, el resultado es NULL también.

Valores NULL y SqlBoolean

La comparación entre cualquier System.Data.SqlTypes devolverá un SqlBoolean. La función IsNull para cada SqlType devuelve un valor SqlBoolean y se puede usar para comprobar si hay valores NULL. Las siguientes tablas truth muestran cómo funcionan los operadores AND, OR y NOT en presencia de un valor NULL. (T = true, F = false y U = Unknown, o NULL).

Truth Table

Descripción de la opción ANSI_NULLS

System.Data.SqlTypes proporciona la misma semántica que cuando se establece la opción ANSI_NULLS en SQL Server. Todos los operadores aritméticos (+, -, *, /, %), operadores a nivel de bit (~, &, |) y la mayoría de las funciones devuelven NULL si alguno de los operandos o argumentos es nulo, excepto en el caso de la propiedad IsNull.

El estándar ANSI SQL-92 no admite columnName = NULL en una cláusula WHERE. En SQL Server, la opción ANSI_NULLS controla la nulabilidad predeterminada en la base de datos y la evaluación de las comparaciones con respecto a los valores NULL. Si ANSI_NULLS está activado (valor predeterminado), se debe usar el operador IS NULL en expresiones al comprobar si hay valores NULL. Por ejemplo, la siguiente comparación genera siempre UNKNOWN cuando ANSI_NULLS está activado:

colname > NULL  

La comparación con una variable que contenga un valor NULL también produce Unknown:

colname > @MyVariable  

Use el predicado IS NULL o IS NOT NULL para probar un valor NULL. Esto puede hacer más compleja la cláusula WHERE. Por ejemplo, la columna TerritoryID de la tabla AdventureWorks Customer permite valores NULL. Si una instrucción SELECT debe comprobar la existencia de valores NULL además de otros, debe incluir un predicado IS NULL:

SELECT CustomerID, AccountNumber, TerritoryID  
FROM AdventureWorks.Sales.Customer  
WHERE TerritoryID IN (1, 2, 3)  
   OR TerritoryID IS NULL  

Si desactiva ANSI_NULLS en SQL Server, puede crear expresiones que usen el operador de igualdad para comparar con NULL. Sin embargo, no puede evitar que diferentes conexiones establezcan opciones NULL para esa conexión. El uso de IS NULL para probar los valores NULL funciona siempre, independientemente de la configuración de ANSI_NULLS para una conexión.

No se admite la desactivación de ANSI_NULLS en un DataSet, que siempre sigue el estándar ANSI SQL-92 para administrar valores NULL en System.Data.SqlTypes.

Asignación de valores NULL

Los valores NULL son especiales, y su semántica de asignación y almacenamiento difiere en diferentes sistemas de tipos y sistemas de almacenamiento. Un valor Dataset está diseñado para usarse con diferentes sistemas de almacenamiento y tipos.

En esta sección se describe la semántica de valores NULL para asignar valores NULL a un parámetro DataColumn en un parámetro DataRow en los distintos sistemas de tipos.

DBNull.Value
Esta asignación es válida para un parámetro DataColumn de cualquier tipo. Si el tipo implementa INullable, DBNull.Value se convierte en el valor NULL fuertemente tipado adecuado.

SqlType.Null
Todos los tipos de datos System.Data.SqlTypes implementan INullable. Si el valor NULL fuertemente tipado se puede convertir en el tipo de datos de la columna mediante operadores de conversión implícitos, la asignación debería avanzar. En caso contrario, se produce una excepción de conversión no válida.

null
Si "null" es un valor válido para el tipo de datos DataColumn especificado, se convierte en el parámetro DbNull.Value adecuado o el parámetro Null asociado al tipo de INullable (SqlType.Null)

derivedUdt.Null
En el caso de las columnas de UDT, los valores NULL se almacenan siempre en función del tipo asociado a DataColumn. Considere el caso de un UDT asociado a un DataColumn que no implementa INullable, mientras su subclase sí lo hace. En este caso, si se asigna un valor NULL fuertemente tipado asociado a la clase derivada, se almacena como un DbNull.Value sin tipo, ya que el almacenamiento NULL siempre es coherente con el tipo de datos de DataColumn.

Nota:

La estructura Nullable<T> o Nullable no se admite actualmente en DataSet.

El valor predeterminado de cualquier instancia System.Data.SqlTypes es NULL.

Los valores NULL de System.Data.SqlTypes son específicos del tipo y no se pueden representar con un valor único, como DbNull. Use la propiedad IsNull para comprobar si hay valores NULL.

Los valores NULL se pueden asignar a un parámetro DataColumn como se muestra en el ejemplo de código siguiente. Puede asignar directamente valores NULL a variables SqlTypes sin desencadenar una excepción.

Ejemplo

En el ejemplo de código siguiente se crea un parámetro DataTable con dos columnas definidas como SqlInt32 y SqlString. El código agrega una fila de valores conocidos y una fila de valores NULL y, a continuación, recorre en iteración DataTable, asignando los valores a las variables y mostrando el resultado en la ventana de la consola.

static void WorkWithSqlNulls()
{
    DataTable table = new();

    // Specify the SqlType for each column.
    DataColumn idColumn =
        table.Columns.Add("ID", typeof(SqlInt32));
    DataColumn descColumn =
        table.Columns.Add("Description", typeof(SqlString));

    // Add some data.
    DataRow nRow = table.NewRow();
    nRow["ID"] = 123;
    nRow["Description"] = "Side Mirror";
    table.Rows.Add(nRow);

    // Add null values.
    nRow = table.NewRow();
    nRow["ID"] = SqlInt32.Null;
    nRow["Description"] = SqlString.Null;
    table.Rows.Add(nRow);

    // Initialize variables to use when
    // extracting the data.
    SqlBoolean isColumnNull = false;
    SqlInt32 idValue = SqlInt32.Zero;
    SqlString descriptionValue = SqlString.Null;

    // Iterate through the DataTable and display the values.
    foreach (DataRow row in table.Rows)
    {
        // Assign values to variables. Note that you
        // do not have to test for null values.
        idValue = (SqlInt32)row["ID"];
        descriptionValue = (SqlString)row["Description"];

        // Test for null value in ID column.
        isColumnNull = idValue.IsNull;

        // Display variable values in console window.
        Console.Write("isColumnNull={0}, ID={1}, Description={2}",
            isColumnNull, idValue, descriptionValue);
        Console.WriteLine();
    }
Private Sub WorkWithSqlNulls()
    Dim table As New DataTable()

    ' Specify the SqlType for each column.
    Dim idColumn As DataColumn = _
      table.Columns.Add("ID", GetType(SqlInt32))
    Dim descColumn As DataColumn = _
      table.Columns.Add("Description", GetType(SqlString))

    ' Add some data.
    Dim row As DataRow = table.NewRow()
    row("ID") = 123
    row("Description") = "Side Mirror"
    table.Rows.Add(row)

    ' Add null values.
    row = table.NewRow()
    row("ID") = SqlInt32.Null
    row("Description") = SqlString.Null
    table.Rows.Add(row)

    ' Initialize variables to use when
    ' extracting the data.
    Dim isColumnNull As SqlBoolean = False
    Dim idValue As SqlInt32 = SqlInt32.Zero
    Dim descriptionValue As SqlString = SqlString.Null

    ' Iterate through the DataTable and display the values.
    For Each row In table.Rows
        ' Assign values to variables. Note that you 
        ' do not have to test for null values.
        idValue = CType(row("ID"), SqlInt32)
        descriptionValue = CType(row("Description"), SqlString)

        ' Test for null value with ID column
        isColumnNull = idValue.IsNull

        ' Display variable values in console window.
        Console.Write("isColumnNull={0}, ID={1}, Description={2}", _
          isColumnNull, idValue, descriptionValue)
        Console.WriteLine()
    Next row
End Sub

Este ejemplo genera los siguientes resultados:

isColumnNull=False, ID=123, Description=Side Mirror  
isColumnNull=True, ID=Null, Description=Null  

Asignación de varias columnas (filas)

DataTable.Add, DataTable.LoadDataRow u otras API que aceptan un parámetro ItemArray que se asigna a una fila, asignan "null" al valor predeterminado de DataColumn. Si un objeto de la matriz contiene DbNull.Value o su homólogo fuertemente tipado, se aplican las mismas reglas que se han descrito anteriormente.

Además, las siguientes reglas se aplican a una instancia de asignaciones NULL DataRow.["columnName"]:

  1. El valor predeterminado es DbNull.Value para todas las columnas excepto aquellas NULL fuertemente tipadas en las que se encuentra el valor NULL fuertemente tipado.

  2. Los valores NULL nunca se escriben durante la serialización en archivos XML (como en "xsi: nil").

  3. Todos los valores no NULL, incluidos los valores predeterminados, siempre se escriben durante la serialización a XML. Esto se diferencia de la semántica de XSD/XML, en la que un valor NULL (xsi:nil) es explícito y el valor predeterminado es implícito (si no está presente en XML, un analizador de validación puede obtenerlo de un esquema XSD asociado). Lo contrario es true para un parámetro DataTable: un valor NULL es implícito y el valor predeterminado es explícito.

  4. Todos los valores de columna que faltan para las filas leídas de la entrada XML se asignan como NULL. A las filas creadas con NewRow o métodos similares se les asigna el valor predeterminado de DataColumn.

  5. El método IsNull devuelve true para DbNull.Value y INullable.Null.

Comparación de valores NULL con SqlTypes y tipos CLR

Al comparar valores NULL, es importante comprender la diferencia entre la forma en que el método Equals evalúa los valores NULL en System.Data.SqlTypes en comparación con el modo en que funciona con tipos CLR. Todos los métodos System.Data.SqlTypes de Equalsutilizan la semántica de base de datos para evaluar valores NULL: si uno o ambos de los valores son NULL, la comparación da como resultado NULL. Por otro lado, el uso del método Equals de CLR en dos System.Data.SqlTypes dará como resultado true si ambos son NULL. Esto refleja la diferencia entre el uso de un método de instancia como el método String.Equals de CLR y el uso del método estático o compartido, SqlString.Equals.

En el ejemplo siguiente se muestra la diferencia en los resultados entre el método SqlString.Equals y el método String.Equals cuando cada uno de ellos pasa un par de valores NULL y, a continuación, un par de cadenas vacías.

    static void CompareNulls()
    {
        // Create two new null strings.
        SqlString a = new();
        SqlString b = new();

        // Compare nulls using static/shared SqlString.Equals.
        Console.WriteLine("SqlString.Equals shared/static method:");
        Console.WriteLine("  Two nulls={0}", SqlStringEquals(a, b));

        // Compare nulls using instance method String.Equals.
        Console.WriteLine();
        Console.WriteLine("String.Equals instance method:");
        Console.WriteLine("  Two nulls={0}", StringEquals(a, b));

        // Make them empty strings.
        a = "";
        b = "";

        // When comparing two empty strings (""), both the shared/static and
        // the instance Equals methods evaluate to true.
        Console.WriteLine();
        Console.WriteLine("SqlString.Equals shared/static method:");
        Console.WriteLine("  Two empty strings={0}", SqlStringEquals(a, b));

        Console.WriteLine();
        Console.WriteLine("String.Equals instance method:");
        Console.WriteLine("  Two empty strings={0}", StringEquals(a, b));
    }

    static string SqlStringEquals(SqlString string1, SqlString string2)
    {
        // SqlString.Equals uses database semantics for evaluating nulls.
        var returnValue = SqlString.Equals(string1, string2).ToString();
        return returnValue;
    }

    static string StringEquals(SqlString string1, SqlString string2)
    {
        // String.Equals uses CLR type semantics for evaluating nulls.
        var returnValue = string1.Equals(string2).ToString();
        return returnValue;
    }
}
Private Sub CompareNulls()
    ' Create two new null strings.
    Dim a As New SqlString
    Dim b As New SqlString

    ' Compare nulls using static/shared SqlString.Equals.
    Console.WriteLine("SqlString.Equals shared/static method:")
    Console.WriteLine("  Two nulls={0}", SqlStringEquals(a, b))

    ' Compare nulls using instance method String.Equals.
    Console.WriteLine()
    Console.WriteLine("String.Equals instance method:")
    Console.WriteLine("  Two nulls={0}", StringEquals(a, b))

    ' Make them empty strings.
    a = ""
    b = ""

    ' When comparing two empty strings (""), both the shared/static and
    ' the instance Equals methods evaluate to true.
    Console.WriteLine()
    Console.WriteLine("SqlString.Equals shared/static method:")
    Console.WriteLine("  Two empty strings={0}", SqlStringEquals(a, b))

    Console.WriteLine()
    Console.WriteLine("String.Equals instance method:")
    Console.WriteLine("  Two empty strings={0}", StringEquals(a, b))
End Sub

Private Function SqlStringEquals(ByVal string1 As SqlString, _
    ByVal string2 As SqlString) As String

    ' SqlString.Equals uses database semantics for evaluating nulls.
    Dim returnValue As String = SqlString.Equals(string1, string2).ToString()
    Return returnValue
End Function

Private Function StringEquals(ByVal string1 As SqlString, _
    ByVal string2 As SqlString) As String

    ' String.Equals uses CLR type semantics for evaluating nulls.
    Dim returnValue As String = string1.Equals(string2).ToString()
    Return returnValue
End Function

El código genera el siguiente resultado:

SqlString.Equals shared/static method:  
  Two nulls=Null  
  
String.Equals instance method:  
  Two nulls=True  
  
SqlString.Equals shared/static method:  
  Two empty strings=True  
  
String.Equals instance method:  
  Two empty strings=True

Vea también