Verifique el nombre de la columna en un objeto SqlDataReader

212

¿Cómo verifico si existe una columna en un SqlDataReader objeto? En mi capa de acceso a datos, he creado un método que construye el mismo objeto para múltiples llamadas a procedimientos almacenados. Uno de los procedimientos almacenados tiene una columna adicional que no es utilizada por los otros procedimientos almacenados. Quiero modificar el método para adaptarlo a cada escenario.

Mi solicitud está escrita en C #.

Michael Kniskern
fuente

Respuestas:

332
public static class DataRecordExtensions
{
    public static bool HasColumn(this IDataRecord dr, string columnName)
    {
        for (int i=0; i < dr.FieldCount; i++)
        {
            if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase))
                return true;
        }
        return false;
    }
}

Usar Exceptions para la lógica de control como en otras respuestas se considera una mala práctica y tiene costos de rendimiento. También envía falsos positivos al perfilador de # excepciones lanzadas y que Dios ayude a cualquiera que configure su depurador para romper las excepciones lanzadas.

GetSchemaTable () también es otra sugerencia en muchas respuestas. Esta no sería una forma preferida de verificar la existencia de un campo, ya que no está implementado en todas las versiones (es abstracto y genera NotSupportedException en algunas versiones de dotnetcore). GetSchemaTable también es excesivo en cuanto a rendimiento, ya que es una función bastante pesada si revisa la fuente .

Recorrer los campos puede tener un pequeño impacto en el rendimiento si lo usa mucho y es posible que desee considerar el almacenamiento en caché de los resultados.

Chad Grant
fuente
¿Qué pasa si se usa un alias? La comparación del nombre fallará.
Murphybro2
Es discutible que usar el flujo de excepción sea una mala práctica. Alguna vez se pensó mal porque es RELATIVAMENTE caro para otros operadores, pero insignificante en una aplicación conectada. Skeet midió 40-118 excepciones por ms dependiendo de la profundidad de la pila en 2006. stackoverflow.com/a/891230/852208 . Además, sin realizar pruebas, es posible que este código sea realmente más lento con su promedio de verificación de casos de la mitad de todas las columnas (aunque todavía es trivial en una aplicación conectada a base de datos) Editaría esta respuesta para incluir solo el párrafo del medio ya que las otras dos son opiniones.
b_levitt
3
@b_levitt no es discutible, es un código basura y no debes confiar en las excepciones para el flujo de control
Chad Grant
Al igual que las dos oraciones que señalé, esa es otra opinión que no está respaldada por ninguna razón más allá del rendimiento en la aplicación puramente computacional. Te reto a configurar tu depurador para que rompa todas las excepciones y deshabilite solo mi código y verás cuánto incluso el marco y otras bibliotecas ya lo están haciendo. El problema con tu consejo es que empuja a los desarrolladores a devolver los códigos que más de acuerdo son un patrón inferior: stackoverflow.com/questions/99683/… . Dicha metodología no pasa la prueba del "pozo del éxito".
b_levitt
Desde la perspectiva del código, su respuesta es válida. Pero su opinión tratando de ponderarlo como una respuesta superior a la respuesta con try / catch (que también maneja alias) está fuera de lugar aquí.
b_levitt
66

Es mucho mejor usar esta función booleana:

r.GetSchemaTable().Columns.Contains(field)

Una llamada, sin excepciones. Puede arrojar excepciones internamente, pero no lo creo.

NOTA: En los comentarios a continuación, descubrimos esto ... el código correcto es en realidad esto:

public static bool HasColumn(DbDataReader Reader, string ColumnName) { 
    foreach (DataRow row in Reader.GetSchemaTable().Rows) { 
        if (row["ColumnName"].ToString() == ColumnName) 
            return true; 
    } //Still here? Column not found. 
    return false; 
}
Jazmín
fuente
55
@Jasmine: ¡Hablé demasiado pronto! Su código busca una columna en la tabla de esquema, no su conjunto de resultados. Debe comparar "campo" (suponiendo que "campo" es el nombre de la columna) con el valor del campo "ColumnName" de cada fila. Rompa cuando lo encuentre, devuelva falso si no lo hace.
Steve J
44
@Steve J: ¿Cuándo el conjunto de resultados NO tendría una columna en GetSchemaTable?
Bendice a Yahu el
1
Para cualquier otra persona confundida, ESTO NO FUNCIONA. Consulte la respuesta a continuación sobre cómo recuperar la fila ColumnName de la tabla de esquema y usarla.
Jason Jackson
3
Sí, esto NO FUNCIONA. ¿Quién lo votó tantas veces? ¡Me hubiera ahorrado mucho tiempo de depuración más tarde si esta respuesta no estuviera aquí!
c00000fd
1
@Jasmine, ¿ambos trabajan? Realmente no. Quite amablemente la primera parte de su respuesta. Lo hubiera hecho yo mismo, ¡pero por tu último comentario!
nawfal
33

Creo que su mejor opción es llamar a GetOrdinal ("columnName") en su DataReader por adelantado, y tomar una IndexOutOfRangeException en caso de que la columna no esté presente.

De hecho, hagamos un método de extensión:

public static bool HasColumn(this IDataRecord r, string columnName)
{
    try
    {
        return r.GetOrdinal(columnName) >= 0;
    }
    catch (IndexOutOfRangeException)
    {
        return false;
    }
}

Editar

Ok, esta publicación está comenzando a obtener algunos votos negativos últimamente, y no puedo eliminarla porque es la respuesta aceptada, así que voy a actualizarla y (espero) tratar de justificar el uso del manejo de excepciones como flujo de control.

La otra forma de lograr esto, según lo publicado por Chad Grant , es recorrer cada campo en el DataReader y hacer una comparación entre mayúsculas y minúsculas para el nombre del campo que está buscando. Esto funcionará realmente bien y, sinceramente, probablemente funcionará mejor que mi método anterior. Ciertamente, nunca usaría el método anterior dentro de un ciclo donde el rendimiento fue un problema.

Puedo pensar en una situación en la que el método try / GetOrdinal / catch funcionará donde el bucle no funciona. Sin embargo, es una situación completamente hipotética en este momento, por lo que es una justificación muy endeble. De todos modos, tenga paciencia conmigo y vea lo que piensa.

Imagine una base de datos que le permite "alias" columnas dentro de una tabla. Imagine que podría definir una tabla con una columna llamada "EmployeeName", pero también darle un alias de "EmpName", y al hacer una selección para cualquiera de los nombres devolvería los datos en esa columna. Conmigo hasta ahora?

Ahora imagine que hay un proveedor de ADO.NET para esa base de datos, y han codificado una implementación de IDataReader que tiene en cuenta los alias de columna.

Ahora, dr.GetName(i)(como se usa en la respuesta de Chad) solo puede devolver una sola cadena, por lo que tiene que devolver solo uno de los "alias" en una columna. Sin embargo,GetOrdinal("EmpName") podría usar la implementación interna de los campos de este proveedor para verificar el alias de cada columna para el nombre que está buscando.

En esta situación hipotética de "columnas con alias", el método try / GetOrdinal / catch sería la única forma de asegurarse de que está comprobando cada variación del nombre de una columna en el conjunto de resultados.

¿Endeble? Por supuesto. Pero vale la pena pensarlo. Sinceramente, prefiero un método HasColumn "oficial" en IDataRecord.

Matt Hamilton
fuente
15
utilizando excepciones para la lógica de control? no no no
Chad Grant
28
Hay una pequeña cosa que todos pasan por alto cuando originalmente publiqué esta pregunta ... Hice la pregunta el 8/12/08 y Matt publicó su respuesta el 17/12/08. Todos apestaron a encontrar una excepción para la lógica de control, pero no proporcionaron una solución alternativa sólida hasta el 5/1/09. Es por eso que originalmente se marcó como la respuesta. Todavía estoy usando esta solución hoy.
Michael Kniskern
19
Esto tendrá un impacto en el rendimiento solo si la columna no estaba allí. Los otros métodos descritos tendrán un impacto de rendimiento y un impacto de rendimiento mayor, cada vez. Si bien generalmente es una mala práctica evitar el uso de manejo de excepciones para el flujo de control, esta solución no debe descartarse sin considerar primero si funciona en su caso.
Nick Harrison
55
+1. Estoy de acuerdo con "No usar excepción para la lógica de control" como una regla de diseño amplia. No significa "evitarlo a toda costa". La respuesta es una solución muy bien documentada y, como dice @Nick, el impacto en el rendimiento (si lo hay ...) solo ocurre cuando la columna no existe.
Larry
2
Usar Excepciones como lógica de control también hace que la depuración sea más engorrosa en mi experiencia. Debe desmarcar "Lanzado" en "Excepciones de Common Language Runtime" y luego, cuando obtenga una excepción real, podría romperse en un controlador en algún lugar y no en la línea que tiene el problema.
cedd
30

En una línea, use esto después de su recuperación de DataReader:

var fieldNames = Enumerable.Range(0, dr.FieldCount).Select(i => dr.GetName(i)).ToArray();

Luego,

if (fieldNames.Contains("myField"))
{
    var myFieldValue = dr["myField"];
    ...

Editar

Una línea mucho más eficiente que no requiere cargar el esquema:

var exists = Enumerable.Range(0, dr.FieldCount).Any(i => string.Equals(dr.GetName(i), fieldName, StringComparison.OrdinalIgnoreCase));
Larry
fuente
Enumera los nombres de campo varias veces / asigna otra matriz para escanear con un contenido, esto sería mucho menos eficaz en el código de alto tráfico.
Chad Grant
@ChadGrant, por supuesto, es por eso que Linq one liner es mucho más eficiente ya que solo realiza una iteración.
Larry
18

Aquí hay una muestra de trabajo para la idea de Jasmin:

var cols = r.GetSchemaTable().Rows.Cast<DataRow>().Select
    (row => row["ColumnName"] as string).ToList(); 

if (cols.Contains("the column name"))
{

}
Chris Ji
fuente
1
Solo si envuelve un intento / captura
Donald.Record
Puede simplificar esta idea con: reader.GetSchemaTable (). Columns.Contains ("myFiled")
Lev Z
El uso de GetSchemaTable () es excesivo (en cuanto a la asignación) para encontrar el nombre de una columna. Echa un vistazo a la fuente github.com/microsoft/referencesource/blob/…
Chad Grant
12

esto funciona para mi:

bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");
Victor Labastida
fuente
El uso de GetSchemaTable () es excesivo (en cuanto a la asignación) para encontrar el nombre de una columna. Y no está implementado en todas las versiones de dotnet core. Echa un vistazo a la fuente github.com/microsoft/referencesource/blob/…
Chad Grant
10

Lo siguiente es simple y funcionó para mí:

 bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);
Paulo Lisboa
fuente
El uso de GetSchemaTable () es excesivo (en cuanto a la asignación) para encontrar el nombre de una columna. Echa un vistazo a la fuente github.com/microsoft/referencesource/blob/…
Chad Grant
8

Si leyó la pregunta, Michael preguntó sobre DataReader, no sobre DataRecord. Consigue tus objetos correctos.

Usando un r.GetSchemaTable().Columns.Contains(field) en un DataRecord funciona, pero devuelve columnas BS (vea la captura de pantalla a continuación).

Para ver si existe una columna de datos Y contiene datos en un DataReader, use las siguientes extensiones:

public static class DataReaderExtensions
{
    /// <summary>
    /// Checks if a column's value is DBNull
    /// </summary>
    /// <param name="dataReader">The data reader</param>
    /// <param name="columnName">The column name</param>
    /// <returns>A bool indicating if the column's value is DBNull</returns>
    public static bool IsDBNull(this IDataReader dataReader, string columnName)
    {
        return dataReader[columnName] == DBNull.Value;
    }

    /// <summary>
    /// Checks if a column exists in a data reader
    /// </summary>
    /// <param name="dataReader">The data reader</param>
    /// <param name="columnName">The column name</param>
    /// <returns>A bool indicating the column exists</returns>
    public static bool ContainsColumn(this IDataReader dataReader, string columnName)
    {
        /// See: http://stackoverflow.com/questions/373230/check-for-column-name-in-a-sqldatareader-object/7248381#7248381
        try
        {
            return dataReader.GetOrdinal(columnName) >= 0;
        }
        catch (IndexOutOfRangeException)
        {
            return false;
        }
    }
}

Uso:

    public static bool CanCreate(SqlDataReader dataReader)
    {
        return dataReader.ContainsColumn("RoleTemplateId") 
            && !dataReader.IsDBNull("RoleTemplateId");
    }

Llamar r.GetSchemaTable().Columnsa un DataReader devuelve columnas BS:

Llamar a GetSchemeTable en un DataReader

Levitikon
fuente
ver comentarios en respuesta Matts
nawfal
¿Qué quiere decir con DataRecord funciona , pero devuelve columnas BS ? ¿Quieres decir que funciona (y da resultados incorrectos)?
nawfal
2
"Haz tus objetos bien". - pero IDataReaderimplementa IDataRecord. Son diferentes interfaces del mismo objeto - al igual que ICollection<T>y IEnumerable<T>son diferentes interfaces de List<T>. IDataReaderpermite avanzar al siguiente registro, mientras que IDataRecordpermite leer desde el registro actual. Los métodos que se utilizan en esta respuesta provienen de la IDataRecordinterfaz. Consulte stackoverflow.com/a/1357743/221708 para obtener una explicación de por qué IDataRecordes preferible declarar el parámetro .
Daniel Schilling
Vota por mostrar por qué r.GetSchemaTable().Columnses una respuesta absolutamente incorrecta a esta pregunta.
Daniel Schilling
GetName () se hereda de la interfaz IDataRecord en IDataReader. Dirigirse a la interfaz base es el código correcto.
Chad Grant
7

Escribí para usuarios de Visual Basic:

Protected Function HasColumnAndValue(ByRef reader As IDataReader, ByVal columnName As String) As Boolean
    For i As Integer = 0 To reader.FieldCount - 1
        If reader.GetName(i).Equals(columnName) Then
            Return Not IsDBNull(reader(columnName))
        End If
    Next

    Return False
End Function

Creo que esto es más poderoso y el uso es:

If HasColumnAndValue(reader, "ID_USER") Then
    Me.UserID = reader.GetDecimal(reader.GetOrdinal("ID_USER")).ToString()
End If
HoLyVieR
fuente
4

Aquí hay una versión linq one liner de la respuesta aceptada:

Enumerable.Range(0, reader.FieldCount).Any(i => reader.GetName(i) == "COLUMN_NAME_GOES_HERE")
Clemente
fuente
comparación de mayúsculas y minúsculas ... ¿por qué?
Chad Grant
4

Aquí la solución de Jasmine en una línea ... (¡una más, aunque simple!):

reader.GetSchemaTable().Select("ColumnName='MyCol'").Length > 0;
Spaark
fuente
El uso de GetSchemaTable () es excesivo (en cuanto a la asignación) para encontrar el nombre de una columna. Echa un vistazo a la fuente github.com/microsoft/referencesource/blob/…
Chad Grant
@ChadGrant Possible. Supongo que uno tiene que elegir sabiamente dependiendo del contexto y la frecuencia con la que es necesario usar esto ...
spaark
3
Hashtable ht = new Hashtable();
    Hashtable CreateColumnHash(SqlDataReader dr)
    {
        ht = new Hashtable();
        for (int i = 0; i < dr.FieldCount; i++)
        {
            ht.Add(dr.GetName(i), dr.GetName(i));
        }
        return ht;
    }

    bool ValidateColumn(string ColumnName)
    {
        return ht.Contains(ColumnName);
    }
Deepak
fuente
3

TLDR:

Muchas respuestas con afirmaciones sobre el rendimiento y las malas prácticas, así que lo aclaro aquí.

La ruta de excepción es más rápida para un mayor número de columnas devueltas, la ruta del bucle es más rápida para un menor número de columnas y el punto de cruce es de alrededor de 11 columnas. Desplácese hacia abajo para ver un gráfico y un código de prueba.

Respuesta completa:

El código para algunas de las principales respuestas funciona, pero hay un debate subyacente aquí para la "mejor" respuesta basada en la aceptación del manejo de excepciones en la lógica y su rendimiento relacionado.

Para aclarar eso, no creo que haya mucha guía con respecto a las excepciones de CATCHING. Microsoft tiene alguna orientación con respecto a LANZAR excepciones. Allí dicen:

NO use excepciones para el flujo normal de control, si es posible.

La primera nota es la indulgencia de "si es posible". Más importante aún, la descripción da este contexto:

framework designers should design APIs so users can write code that does not throw exceptions

Lo que eso significa es que si está escribiendo una API que podría ser consumida por otra persona, bríndeles la capacidad de navegar una excepción sin un intento / captura. Por ejemplo, proporcione un TryParse con su método Parse de lanzamiento de excepciones. Sin embargo, en ninguna parte esto dice que no deberías atrapar una excepción.

Además, como señala otro usuario, las capturas siempre han permitido el filtrado por tipo y, de alguna manera, recientemente permiten un mayor filtrado a través de la cláusula when . Esto parece un desperdicio de características del lenguaje si no se supone que las usemos.

Se puede decir que hay ALGUNOS costos para una excepción lanzada, y ese costo PUEDE afectar el rendimiento en un ciclo pesado. Sin embargo, también se puede decir que el costo de una excepción será insignificante en una "aplicación conectada". El costo real se investigó hace más de una década: https://stackoverflow.com/a/891230/852208 En otras palabras, el costo de una conexión y consulta de una base de datos probablemente eclipsará el de una excepción lanzada.

Aparte de eso, quería determinar qué método es realmente más rápido. Como se esperaba, no hay una respuesta concreta.

Cualquier código que recorra las columnas se vuelve más lento a medida que existe el número de columnas. También se puede decir que cualquier código que se base en excepciones se ralentizará dependiendo de la velocidad con la que no se encuentre la consulta.

Tomando las respuestas de Chad Grant y Matt Hamilton, ejecuté ambos métodos con hasta 20 columnas y una tasa de error de hasta el 50% (el OP indicó que estaba usando estas dos pruebas entre diferentes procesos, por lo que supuse que solo dos) .

Aquí están los resultados, trazados con LinqPad: Resultados: la Serie 1 es Loop, 2 es Excepción

Los zigzags aquí son tasas de fallas (columna no encontrada) dentro de cada recuento de columnas.

En conjuntos de resultados más estrechos, el bucle es una buena opción. Sin embargo, el método GetOrdinal / Exception no es tan sensible al número de columnas y comienza a superar el método de bucle alrededor de 11 columnas.

Dicho esto, realmente no tengo un rendimiento de preferencia sabio ya que 11 columnas suenan razonables ya que un número promedio de columnas devueltas en una aplicación completa. En cualquier caso, estamos hablando de fracciones de milisegundos aquí.

Sin embargo, desde un aspecto de simplicidad de código y soporte de alias, probablemente iría con la ruta GetOrdinal.

Aquí está la prueba en forma de linqpad. Siéntase libre de volver a publicar con su propio método:

void Main()
{
    var loopResults = new List<Results>();
    var exceptionResults = new List<Results>();
    var totalRuns = 10000;
    for (var colCount = 1; colCount < 20; colCount++)
    {
        using (var conn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=master;Integrated Security=True;"))
        {
            conn.Open();

            //create a dummy table where we can control the total columns
            var columns = String.Join(",",
                (new int[colCount]).Select((item, i) => $"'{i}' as col{i}")
            );
            var sql = $"select {columns} into #dummyTable";
            var cmd = new SqlCommand(sql,conn);
            cmd.ExecuteNonQuery();

            var cmd2 = new SqlCommand("select * from #dummyTable", conn);

            var reader = cmd2.ExecuteReader();
            reader.Read();

            Func<Func<IDataRecord, String, Boolean>, List<Results>> test = funcToTest =>
            {
                var results = new List<Results>();
                Random r = new Random();
                for (var faultRate = 0.1; faultRate <= 0.5; faultRate += 0.1)
                {
                    Stopwatch stopwatch = new Stopwatch();
                    stopwatch.Start();
                    var faultCount=0;
                    for (var testRun = 0; testRun < totalRuns; testRun++)
                    {
                        if (r.NextDouble() <= faultRate)
                        {
                            faultCount++;
                            if(funcToTest(reader, "colDNE"))
                                throw new ApplicationException("Should have thrown false");
                        }
                        else
                        {
                            for (var col = 0; col < colCount; col++)
                            {
                                if(!funcToTest(reader, $"col{col}"))
                                    throw new ApplicationException("Should have thrown true");
                            }
                        }
                    }
                    stopwatch.Stop();
                    results.Add(new UserQuery.Results{
                        ColumnCount = colCount, 
                        TargetNotFoundRate = faultRate,
                        NotFoundRate = faultCount * 1.0f / totalRuns, 
                        TotalTime=stopwatch.Elapsed
                    });
                }
                return results;
            };
            loopResults.AddRange(test(HasColumnLoop));

            exceptionResults.AddRange(test(HasColumnException));

        }

    }
    "Loop".Dump();
    loopResults.Dump();

    "Exception".Dump();
    exceptionResults.Dump();

    var combinedResults = loopResults.Join(exceptionResults,l => l.ResultKey, e=> e.ResultKey, (l, e) => new{ResultKey = l.ResultKey, LoopResult=l.TotalTime, ExceptionResult=e.TotalTime});
    combinedResults.Dump();
    combinedResults
        .Chart(r => r.ResultKey, r => r.LoopResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
        .AddYSeries(r => r.ExceptionResult.Milliseconds * 1.0 / totalRuns, LINQPad.Util.SeriesType.Line)
        .Dump();
}
public static bool HasColumnLoop(IDataRecord dr, string columnName)
{
    for (int i = 0; i < dr.FieldCount; i++)
    {
        if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase))
            return true;
    }
    return false;
}

public static bool HasColumnException(IDataRecord r, string columnName)
{
    try
    {
        return r.GetOrdinal(columnName) >= 0;
    }
    catch (IndexOutOfRangeException)
    {
        return false;
    }
}

public class Results
{
    public double NotFoundRate { get; set; }
    public double TargetNotFoundRate { get; set; }
    public int ColumnCount { get; set; }
    public double ResultKey {get => ColumnCount + TargetNotFoundRate;}
    public TimeSpan TotalTime { get; set; }


}
b_levitt
fuente
1
Claramente tienes algún tipo de obsesión extraña con excepciones. Un mejor enfoque sería simplemente almacenar en caché la ubicación de la columna en una búsqueda estática para el rendimiento y usar la búsqueda de enteros
Chad Grant
Otro problema con el uso de excepciones como flujo de control es que se muestran en el generador de perfiles como # de excepciones lanzadas cuando en su código sugerido son intencionales ... no excepciones. Sin mencionar la configuración de su depurador para romper las excepciones lanzadas. Básicamente, informar errores que no son errores. No deberías estar haciendo esto.
Chad Grant
1
También hay contadores para finalmente / seg, y filtros / seg. ¿Son esos malos también? Yo diría que es una posible advertencia, la primera real que ha proporcionado. Los contadores son solo información. No significan nada a menos que correspondan a un problema de rendimiento, y en este caso ya he mostrado el punto donde las excepciones tienen MEJOR rendimiento. También he indicado que el marco y las bibliotecas ya arrojan muchas excepciones. Tengo una instancia de estudio visual lanzando 60 ex / s en este momento. Las excepciones no son errores a menos que se descubran.
b_levitt
Gran análisis Usé sus resultados en mi nueva respuesta.
yazanpro
1

Este código corrige los problemas que Levitikon tenía con su código: (adaptado de: [1]: http://msdn.microsoft.com/en-us/library/system.data.datatablereader.getschematable.aspx )

public List<string> GetColumnNames(SqlDataReader r)
{
    List<string> ColumnNames = new List<string>();
    DataTable schemaTable = r.GetSchemaTable();
    DataRow row = schemaTable.Rows[0];
    foreach (DataColumn col in schemaTable.Columns)
    {
        if (col.ColumnName == "ColumnName") 
        { 
            ColumnNames.Add(row[col.Ordinal].ToString()); 
            break; 
        }
    }
    return ColumnNames;
}

La razón para obtener todos esos nombres de columna inútiles y no el nombre de la columna de su tabla ... es porque está obteniendo el nombre de la columna de esquema (es decir, los nombres de columna para la tabla de Esquema)

NOTA: parece que solo devuelve el nombre de la primera columna ...

EDITAR: código corregido que devuelve el nombre de todas las columnas, pero no puede usar un SqlDataReader para hacerlo

public List<string> ExecuteColumnNamesReader(string command, List<SqlParameter> Params)
{
    List<string> ColumnNames = new List<string>();
    SqlDataAdapter da = new SqlDataAdapter();
    string connection = ""; // your sql connection string
    SqlCommand sqlComm = new SqlCommand(command, connection);
    foreach (SqlParameter p in Params) { sqlComm.Parameters.Add(p); }
    da.SelectCommand = sqlComm;
    DataTable dt = new DataTable();
    da.Fill(dt);
    DataRow row = dt.Rows[0];
    for (int ordinal = 0; ordinal < dt.Columns.Count; ordinal++)
    {
        string column_name = dt.Columns[ordinal].ColumnName;
        ColumnNames.Add(column_name);
    }
    return ColumnNames; // you can then call .Contains("name") on the returned collection
}
NeoH4x0r
fuente
O en una línea return r.GetSchemaTable().Rows.Cast<DataRow>().Select(x => (string)x["ColumnName"]).ToList();:)
nawfal
El uso de GetSchemaTable () es excesivo (en cuanto a la asignación) para encontrar el nombre de una columna. Echa un vistazo a la fuente github.com/microsoft/referencesource/blob/…
Chad Grant
1

Para mantener su código robusto y limpio, use una función de extensión única, como esta:

    Public Module Extensions

        <Extension()>
        Public Function HasColumn(r As SqlDataReader, columnName As String) As Boolean

            Return If(String.IsNullOrEmpty(columnName) OrElse r.FieldCount = 0, False, Enumerable.Range(0, r.FieldCount).Select(Function(i) r.GetName(i)).Contains(columnName, StringComparer.OrdinalIgnoreCase))

        End Function

    End Module
Michael B
fuente
0

Tampoco llegué GetSchemaTablea trabajar, hasta que encontré este camino .

Básicamente hago esto:

Dim myView As DataView = dr.GetSchemaTable().DefaultView
myView.RowFilter = "ColumnName = 'ColumnToBeChecked'"

If myView.Count > 0 AndAlso dr.GetOrdinal("ColumnToBeChecked") <> -1 Then
  obj.ColumnToBeChecked = ColumnFromDb(dr, "ColumnToBeChecked")
End If
David Andersson
fuente
0
public static bool DataViewColumnExists(DataView dv, string columnName)
{
    return DataTableColumnExists(dv.Table, columnName);
}

public static bool DataTableColumnExists(DataTable dt, string columnName)
{
    string DebugTrace = "Utils::DataTableColumnExists(" + dt.ToString() + ")";
    try
    {
        return dt.Columns.Contains(columnName);
    }
    catch (Exception ex)
    {
        throw new MyExceptionHandler(ex, DebugTrace);
    }
}

Columns.Contains Por cierto, no distingue entre mayúsculas y minúsculas.

RBAFF79
fuente
Contiene () no arroja excepciones, este código no tiene sentido. Solo capturaría excepciones de puntero nulo.
Chad Grant
0

En su situación particular (todos los procedimientos tienen las mismas columnas excepto 1 que tiene 1 columna adicional), será mejor y más rápido verificar el lector. Propiedad FieldCount para distinguir entre ellos.

const int NormalColCount=.....
if(reader.FieldCount > NormalColCount)
{
// Do something special
}

Sé que es una publicación antigua, pero decidí responder para ayudar a otros en la misma situación. También puede (por razones de rendimiento) mezclar esta solución con la solución iterativa de la solución.

pkrzemo
fuente
Por favor, nombre la solución a la que se refiere. ¿Qué dos soluciones se deben mezclar?
Pablo Jomer
0

Mi clase de acceso a datos debe ser compatible con versiones anteriores, por lo que podría estar intentando acceder a una columna en una versión donde aún no existe en la base de datos. Tenemos algunos conjuntos de datos bastante grandes que se devuelven, así que no soy un gran admirador de un método de extensión que tiene que iterar la colección de columnas DataReader para cada propiedad.

Tengo una clase de utilidad que crea una lista privada de columnas y luego tiene un método genérico que intenta resolver un valor basado en el nombre de una columna y el tipo de parámetro de salida.

private List<string> _lstString;

public void GetValueByParameter<T>(IDataReader dr, string parameterName, out T returnValue)
{
    returnValue = default(T);

    if (!_lstString.Contains(parameterName))
    {
        Logger.Instance.LogVerbose(this, "missing parameter: " + parameterName);
        return;
    }

    try
    {
        if (dr[parameterName] != null && [parameterName] != DBNull.Value)
            returnValue = (T)dr[parameterName];
    }
    catch (Exception ex)
    {
        Logger.Instance.LogException(this, ex);
    }
}

/// <summary>
/// Reset the global list of columns to reflect the fields in the IDataReader
/// </summary>
/// <param name="dr">The IDataReader being acted upon</param>
/// <param name="NextResult">Advances IDataReader to next result</param>
public void ResetSchemaTable(IDataReader dr, bool nextResult)
{
    if (nextResult)
        dr.NextResult();

    _lstString = new List<string>();

    using (DataTable dataTableSchema = dr.GetSchemaTable())
    {
        if (dataTableSchema != null)
        {
            foreach (DataRow row in dataTableSchema.Rows)
            {
                _lstString.Add(row[dataTableSchema.Columns["ColumnName"]].ToString());
            }
        }
    }
}

Entonces puedo llamar a mi código así

using (var dr = ExecuteReader(databaseCommand))
{
    int? outInt;
    string outString;

    Utility.ResetSchemaTable(dr, false);        
    while (dr.Read())
    {
        Utility.GetValueByParameter(dr, "SomeColumn", out outInt);
        if (outInt.HasValue) myIntField = outInt.Value;
    }

    Utility.ResetSchemaTable(dr, true);
    while (dr.Read())
    {
        Utility.GetValueByParameter(dr, "AnotherColumn", out outString);
        if (!string.IsNullOrEmpty(outString)) myIntField = outString;
    }
}
Tresto
fuente
0

La clave de todo el problema está aquí :

if (-1 == index) {
    throw ADP.IndexOutOfRange(fieldName);
}

Si las tres líneas a las que se hace referencia (actualmente las líneas 72, 73 y 74) se eliminan, puede verificarlas fácilmente para -1determinar si la columna no existe.

La única forma de evitar esto mientras se garantiza el rendimiento nativo es utilizar una Reflectionimplementación basada, como la siguiente:

Usos

using System;
using System.Data;
using System.Reflection;
using System.Data.SqlClient;
using System.Linq;
using System.Web.Compilation; // I'm not sure what the .NET Core equivalent to BuildManager.cs

El método de extensión basado en Reflection:

/// Gets the column ordinal, given the name of the column.
/// </summary>
/// <param name="reader"></param>
/// <param name="name">The name of the column.</param>
/// <returns> The zero-based column ordinal. -1 if the column does not exist.</returns>
public static int GetOrdinalSoft(this SqlDataReader reader, string name)
{
    try
    {
        // Note that "Statistics" will not be accounted for in this implemenation
        // If you have SqlConnection.StatisticsEnabled set to true (the default is false), you probably don't want to use this method
        // All of the following logic is inspired by the actual implementation of the framework:
        // https://referencesource.microsoft.com/#System.Data/fx/src/data/System/Data/SqlClient/SqlDataReader.cs,d66096b6f57cac74
        if (name == null)
            throw new ArgumentNullException("fieldName");

        Type sqlDataReaderType = typeof(SqlDataReader);
        object fieldNameLookup = sqlDataReaderType.GetField("_fieldNameLookup", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(reader);
        Type fieldNameLookupType;
        if (fieldNameLookup == null)
        {
            MethodInfo checkMetaDataIsReady = sqlDataReaderType.GetRuntimeMethods().First(x => x.Name == "CheckMetaDataIsReady" && x.GetParameters().Length == 0);
            checkMetaDataIsReady.Invoke(reader, null);
            fieldNameLookupType = BuildManager.GetType("System.Data.ProviderBase.FieldNameLookup", true, false);
            ConstructorInfo ctor = fieldNameLookupType.GetConstructor(new[] { typeof(SqlDataReader), typeof(int) });
            fieldNameLookup = ctor.Invoke(new object[] { reader, sqlDataReaderType.GetField("_defaultLCID", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(reader) });
        }
        else
            fieldNameLookupType = fieldNameLookup.GetType();

        MethodInfo indexOf = fieldNameLookupType.GetMethod("IndexOf", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(string) }, null);

        return (int)indexOf.Invoke(fieldNameLookup, new object[] { name });
    }
    catch
    {
        // .NET Implemenation might have changed, revert back to the classic solution.
        if (reader.FieldCount > 11) // Performance observation by b_levitt
        {
            try
            {
                return reader.GetOrdinal(name);
            }
            catch
            {
                return -1;
            }
        }
        else
        {
            var exists = Enumerable.Range(0, reader.FieldCount).Any(i => string.Equals(reader.GetName(i), name, StringComparison.OrdinalIgnoreCase));
            if (exists)
                return reader.GetOrdinal(name);
            else
                return -1;
        }
    }
}
yazanpro
fuente
-1

Qué tal si

if (dr.GetSchemaTable().Columns.Contains("accounttype"))
   do something
else
   do something

Probablemente no sería tan eficiente en un bucle

Skadoosh
fuente
Vea la respuesta de Levitikon para ver el tipo de cosas que dr.GetSchemaTable().Columnscontiene: no es lo que está buscando.
Daniel Schilling
-1

Aunque no existe un método expuesto públicamente, existe un método en la clase interna System.Data.ProviderBase.FieldNameLookupqueSqlDataReader basa.

Para acceder a él y obtener un rendimiento nativo, debe usar el ILGenerator para crear un método en tiempo de ejecución. El siguiente código le dará acceso directo a int IndexOf(string fieldName)la System.Data.ProviderBase.FieldNameLookupclase, así como realizar la contabilidad que lo SqlDataReader.GetOrdinal()hace para que no haya efectos secundarios. El código generado refleja el existente SqlDataReader.GetOrdinal()excepto que llama en FieldNameLookup.IndexOf()lugar de FieldNameLookup.GetOrdinal(). El GetOrdinal()método llama a la IndexOf()función y genera una excepción si -1se devuelve, por lo que omitimos ese comportamiento.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;
using System.Reflection.Emit;

public static class SqlDataReaderExtensions {

   private delegate int IndexOfDelegate(SqlDataReader reader, string name);
   private static IndexOfDelegate IndexOf;

   public static int GetColumnIndex(this SqlDataReader reader, string name) {
      return name == null ? -1 : IndexOf(reader, name);
   }

   public static bool ContainsColumn(this SqlDataReader reader, string name) {
      return name != null && IndexOf(reader, name) >= 0;
   }

   static SqlDataReaderExtensions() {
      Type typeSqlDataReader = typeof(SqlDataReader);
      Type typeSqlStatistics = typeSqlDataReader.Assembly.GetType("System.Data.SqlClient.SqlStatistics", true);
      Type typeFieldNameLookup = typeSqlDataReader.Assembly.GetType("System.Data.ProviderBase.FieldNameLookup", true);

      BindingFlags staticflags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Static;
      BindingFlags instflags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance;

      DynamicMethod dynmethod = new DynamicMethod("SqlDataReader_IndexOf", typeof(int), new Type[2]{ typeSqlDataReader, typeof(string) }, true);
      ILGenerator gen = dynmethod.GetILGenerator();
      gen.DeclareLocal(typeSqlStatistics);
      gen.DeclareLocal(typeof(int));

      // SqlStatistics statistics = (SqlStatistics) null;
      gen.Emit(OpCodes.Ldnull);
      gen.Emit(OpCodes.Stloc_0);
      // try {
      gen.BeginExceptionBlock();
      //    statistics = SqlStatistics.StartTimer(this.Statistics);
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Call, typeSqlDataReader.GetProperty("Statistics", instflags | BindingFlags.GetProperty, null, typeSqlStatistics, Type.EmptyTypes, null).GetMethod);
      gen.Emit(OpCodes.Call, typeSqlStatistics.GetMethod("StartTimer", staticflags | BindingFlags.InvokeMethod, null, new Type[] { typeSqlStatistics }, null));
      gen.Emit(OpCodes.Stloc_0); //statistics
      //    if(this._fieldNameLookup == null) {
      Label branchTarget = gen.DefineLabel();
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldfld, typeSqlDataReader.GetField("_fieldNameLookup", instflags | BindingFlags.GetField));
      gen.Emit(OpCodes.Brtrue_S, branchTarget);
      //       this.CheckMetaDataIsReady();
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Call, typeSqlDataReader.GetMethod("CheckMetaDataIsReady", instflags | BindingFlags.InvokeMethod, null, Type.EmptyTypes, null));
      //       this._fieldNameLookup = new FieldNameLookup((IDataRecord)this, this._defaultLCID);
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldfld, typeSqlDataReader.GetField("_defaultLCID", instflags | BindingFlags.GetField));
      gen.Emit(OpCodes.Newobj, typeFieldNameLookup.GetConstructor(instflags, null, new Type[] { typeof(IDataReader), typeof(int) }, null));
      gen.Emit(OpCodes.Stfld, typeSqlDataReader.GetField("_fieldNameLookup", instflags | BindingFlags.SetField));
      //    }
      gen.MarkLabel(branchTarget);
      gen.Emit(OpCodes.Ldarg_0); //this
      gen.Emit(OpCodes.Ldfld, typeSqlDataReader.GetField("_fieldNameLookup", instflags | BindingFlags.GetField));
      gen.Emit(OpCodes.Ldarg_1); //name
      gen.Emit(OpCodes.Call, typeFieldNameLookup.GetMethod("IndexOf", instflags | BindingFlags.InvokeMethod, null, new Type[] { typeof(string) }, null));
      gen.Emit(OpCodes.Stloc_1); //int output
      Label leaveProtectedRegion = gen.DefineLabel();
      gen.Emit(OpCodes.Leave_S, leaveProtectedRegion);
      // } finally {
      gen.BeginFaultBlock();
      //    SqlStatistics.StopTimer(statistics);
      gen.Emit(OpCodes.Ldloc_0); //statistics
      gen.Emit(OpCodes.Call, typeSqlStatistics.GetMethod("StopTimer", staticflags | BindingFlags.InvokeMethod, null, new Type[] { typeSqlStatistics }, null));
      // }
      gen.EndExceptionBlock();
      gen.MarkLabel(leaveProtectedRegion);
      gen.Emit(OpCodes.Ldloc_1);
      gen.Emit(OpCodes.Ret);

      IndexOf = (IndexOfDelegate)dynmethod.CreateDelegate(typeof(IndexOfDelegate));
   }

}
Derek Ziemba
fuente
1
El código interno hace casi exactamente lo mismo que está haciendo mi respuesta, sin la necesidad de esta extraña reflexión / delegado. Está almacenando en caché la búsqueda por instancia de objeto, lo que no sería beneficioso, ya que en el mundo real desea almacenar en caché los ordinales la primera vez que se ejecuta la consulta y usar ese caché durante la vida útil de la aplicación, no construir un nuevo caché en cada consulta.
Chad Grant
-1

este trabajo para mi

public static class DataRecordExtensions
{
        public static bool HasColumn(IDataReader dataReader, string columnName)
        {
            dataReader.GetSchemaTable().DefaultView.RowFilter = $"ColumnName= '{columnName}'";
            return (dataReader.GetSchemaTable().DefaultView.Count > 0);
        }
}
joshua saucedo
fuente