Verifique o nome da coluna em um objeto SqlDataReader


212

Como verifico se existe uma coluna em um SqlDataReaderobjeto? Na minha camada de acesso a dados, criei um método que cria o mesmo objeto para várias chamadas de procedimentos armazenados. Um dos procedimentos armazenados possui uma coluna adicional que não é usada pelos outros procedimentos armazenados. Quero modificar o método para acomodar todos os cenários.

Meu aplicativo está escrito em c #.

Respostas:


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 lógica de controle, como em algumas outras respostas, é considerado uma prática ruim e tem custos de desempenho. Ele também envia falsos positivos para o criador de perfil de # exceções lançadas e que Deus ajude qualquer um que esteja configurando seu depurador para quebrar as exceções lançadas.

GetSchemaTable () também é outra sugestão em muitas respostas. Essa não seria uma maneira preferida de verificar a existência de um campo, pois ele não é implementado em todas as versões (é abstrato e lança NotSupportedException em algumas versões do dotnetcore). GetSchemaTable também é um desempenho exagerado, pois é uma função bastante pesada se você verificar a fonte .

O loop pelos campos pode ter um pequeno impacto no desempenho se você o usar muito e considerar o armazenamento em cache dos resultados.


E se um alias for usado? A comparação do nome falhará.
Murphybro2

É discutível que usar o fluxo de exceção seja uma prática ruim. Era considerado ruim porque é RELATIVAMENTE caro para outros operadores, mas insignificante em um aplicativo conectado. O Skeet mediu 40-118 exceções por ms, dependendo da profundidade da pilha desde 2006. stackoverflow.com/a/891230/852208 . Além disso, sem o teste, é possível que esse código seja realmente mais lento, pois verifica a média de todas as colunas (embora ainda seja trivial em um aplicativo conectado ao banco de dados). Eu editaria esta resposta para incluir apenas o parágrafo do meio, pois os outros dois são opiniões.
b_levitt 21/05/19

3
@b_levitt não é discutível, é código porcaria e você não deve confiar em exceções para controle de fluxo
Chad Grant

Como as duas frases que apontei, essa é mais uma opinião que não é suportada com nenhuma justificativa além do desempenho em aplicações puramente computacionais. Desafio você a definir seu depurador para quebrar todas as exceções e desabilitar apenas meu código, e você verá o quanto a estrutura e outras bibliotecas já estão fazendo isso. Concordo é um padrão inferior: stackoverflow.com/questions/99683/… . Essa metodologia falha no teste do "poço do sucesso".
b_levitt

De uma perspectiva de código, sua resposta é válida. Mas sua opinião, tentando ponderá-la como uma resposta superior à resposta com o try / catch (que também lida com aliases) está fora de lugar aqui.
b_levitt

66

É muito melhor usar esta função booleana:

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

Uma chamada - sem exceções. Pode lançar exceções internamente, mas acho que não.

NOTA: Nos comentários abaixo, descobrimos isso ... o código correto é realmente este:

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; 
}

5
@ Jasmine: falei cedo demais! Seu código procura uma coluna na tabela de esquema, não seu conjunto de resultados. Você precisa comparar "campo" (assumindo que "campo" é o nome da coluna) com o valor do campo "ColumnName" de cada linha. Quebre quando o encontrar, retorne false se não encontrar.
21139 Steve J

4
@ Steve J: Quando o conjunto de resultados NÃO teria uma coluna no GetSchemaTable?
Bless Yahu 07/08/09

1
Para qualquer um mais confuso, isso não funciona. Consulte A resposta abaixo sobre como recuperar a linha ColumnName da tabela de esquema e usá-la.
Jason Jackson

3
Sim, isso NÃO FUNCIONA. Quem votou tantas vezes ??? Isso me pouparia muito tempo de depuração mais tarde se essa resposta não estivesse aqui!
C00000fd

1
@ Jasmine ambos trabalham? Na verdade não. Remova a primeira parte da sua resposta. Eu teria feito a mim mesmo, mas pelo seu último comentário!
Nawfal

33

Acho que sua melhor aposta é chamar GetOrdinal ("columnName") no seu DataReader na frente e pegar uma IndexOutOfRangeException no caso de a coluna não estar presente.

De fato, vamos criar um método de extensão:

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

Editar

Ok, este post está começando a receber alguns votos negativos recentemente, e não posso excluí-lo porque é a resposta aceita, então vou atualizá-lo e (espero) tentar justificar o uso do tratamento de exceções como controle de fluxo.

A outra maneira de conseguir isso, conforme publicado por Chad Grant , é percorrer cada campo no DataReader e fazer uma comparação sem distinção entre maiúsculas e minúsculas para o nome do campo que você está procurando. Isso funcionará muito bem e, na verdade, provavelmente terá um desempenho melhor que o meu método acima. Certamente eu nunca usaria o método acima dentro de um loop em que o desempenho era um problema.

Eu posso pensar em uma situação em que o método try / GetOrdinal / catch funcionará onde o loop não funciona. É, no entanto, uma situação completamente hipotética no momento, por isso é uma justificativa muito frágil. Independentemente disso, tenha paciência comigo e veja o que pensa.

Imagine um banco de dados que permita "alias" colunas dentro de uma tabela. Imagine que eu poderia definir uma tabela com uma coluna chamada "EmployeeName", mas também fornecer um apelido de "EmpName", e fazer uma seleção para qualquer nome retornaria os dados nessa coluna. Comigo até agora?

Agora imagine que exista um provedor ADO.NET para esse banco de dados, e eles codificaram uma implementação do IDataReader, que leva em consideração os aliases da coluna.

Agora, dr.GetName(i)(como usado na resposta do Chade), pode retornar apenas uma única sequência, portanto, ele deve retornar apenas um dos "aliases" em uma coluna. No entanto, você GetOrdinal("EmpName")pode usar a implementação interna dos campos desse provedor para verificar o alias de cada coluna para o nome que você está procurando.

Nessa situação hipotética de "colunas com alias", o método try / GetOrdinal / catch seria a única maneira de garantir que você esteja verificando todas as variações do nome de uma coluna no conjunto de resultados.

Frágil? Certo. Mas vale a pena pensar. Honestamente, eu prefiro um método HasColumn "oficial" no IDataRecord.


15
usando exceções para a lógica de controle? não não não
Chad Grant

28
Há uma pequena coisa que todo mundo esquece quando eu postei originalmente essa pergunta ... Eu fiz a pergunta em 8/12/08 e Matt postou sua resposta em 17/12/08. Todo mundo fedia a capturar uma exceção para a lógica de controle, mas não forneceu uma solução alternativa sólida até 1/5/09. É por isso que foi originalmente marcado como a resposta. Ainda estou usando esta solução hoje.
Michael Kniskern

19
Isso terá um desempenho atingido apenas se a coluna não estiver lá. Os outros métodos descritos terão um impacto no desempenho e um impacto maior no desempenho, sempre. Embora seja geralmente uma prática recomendada evitar o tratamento de exceções para o fluxo de controle, essa solução não deve ser descartada sem antes considerar se funciona no seu caso.
Nick Harrison

5
+1. Eu estou bem com "Não use exceção para lógica de controle" como uma regra de design geral. Isso não significa "evitá-lo a todo custo". A resposta é uma solução muito bem documentada e, como o @Nick diz, o impacto no desempenho (se houver ..) ocorre apenas quando a coluna não existe.
Larry

2
Usar exceções como lógica de controle também torna a depuração mais complicada na minha experiência. É necessário desmarcar "Thrown" em "Common Language Runtime Exceptions" e, quando você receber uma exceção real, ela pode ser quebrada em um manipulador em algum lugar e não na linha que está com o problema.
CEDD

30

Em uma linha, use isso após a recuperação do DataReader:

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

Então,

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

Editar

Linha única muito mais eficiente que não requer o carregamento do esquema:

var exists = Enumerable.Range(0, dr.FieldCount).Any(i => string.Equals(dr.GetName(i), fieldName, StringComparison.OrdinalIgnoreCase));

Você está enumerando os nomes dos campos várias vezes / alocando outra matriz para varrer com um contém, isso teria muito menos desempenho em código de alto tráfego.
Chad Grant

@ChadGrant, é claro, é por isso que o linq one liner é muito mais eficiente, pois realiza apenas uma iteração.
Larry

18

Aqui está uma amostra de trabalho da ideia de Jasmin:

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

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

}

1
Apenas se você envolver um try / catch em torno dele
Donald.Record

Você pode simplificar essa idéia com: reader.GetSchemaTable (). Columns.Contains ("myFiled")
Lev Z

usar GetSchemaTable () é excessivo (em termos de alocação) apenas para encontrar um nome de coluna. Verifique a fonte github.com/microsoft/referencesource/blob/…
Chad Grant

12

isso funciona para mim:

bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");

usar GetSchemaTable () é excessivo (em termos de alocação) apenas para encontrar um nome de coluna. E não é implementado em todas as versões do dotnet core. Verifique a fonte github.com/microsoft/referencesource/blob/…
Chad Grant


8

Se você leu a pergunta, Michael perguntou sobre o DataReader, não sobre o pessoal do DataRecord. Acerte seus objetos.

Usar um r.GetSchemaTable().Columns.Contains(field)em um DataRecord funciona, mas retorna colunas BS (veja a captura de tela abaixo).

Para verificar se existe uma coluna de dados E contém dados em um DataReader, use as seguintes extensões:

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");
    }

Chamar r.GetSchemaTable().Columnsum DataReader retorna colunas BS:

Chamando GetSchemeTable em um DataReader


veja comentários em Matts answer
nawfal 8/13

O que você quer dizer com DataRecord funciona , mas retorna colunas BS ? Você quer dizer que ele roda (e dá resultados errados)?
Nawfal

2
"Acerte seus objetos." - mas IDataReaderimplementa IDataRecord. Eles são interfaces diferentes do mesmo objeto - exatamente como ICollection<T>e IEnumerable<T>são interfaces diferentes List<T>. IDataReaderpermite avançar para o próximo registro, enquanto IDataRecordpermite a leitura do registro atual. Os métodos que estão sendo usados ​​nesta resposta são todos da IDataRecordinterface. Consulte stackoverflow.com/a/1357743/221708 para obter uma explicação sobre por que IDataRecordé preferível declarar o parâmetro .
Daniel Schilling

Voto positivo por mostrar por que r.GetSchemaTable().Columnsé uma resposta absolutamente errada para esta pergunta.
Daniel Schilling

GetName () é herdado da interface IDataRecord no IDataReader. A segmentação da interface base é o código correto.
Chad Grant

7

Eu escrevi para usuários do 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

Eu acho que isso é mais poderoso e o uso é:

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

4

Aqui está uma versão linq de uma linha da resposta aceita:

Enumerable.Range(0, reader.FieldCount).Any(i => reader.GetName(i) == "COLUMN_NAME_GOES_HERE")

comparação sensível a maiúsculas ... por quê?
Chad Grant

4

Aqui a solução do Jasmine em uma linha ... (mais uma, muito simples!):

reader.GetSchemaTable().Select("ColumnName='MyCol'").Length > 0;

usar GetSchemaTable () é excessivo (em termos de alocação) apenas para encontrar um nome de coluna. Verifique a fonte github.com/microsoft/referencesource/blob/…
Chad Grant

@ChadGrant Possível. Eu acho que um tem que escolher sabiamente, dependendo do contexto e da frequência é necessário para usar esta ...
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);
    }

3

TLDR:

Muitas respostas com alegações sobre desempenho e práticas inadequadas, por isso esclareço isso aqui.

A rota de exceção é mais rápida para um número maior de colunas retornadas, a rota de loop é mais rápida para um número menor de colunas e o ponto de cruzamento é de cerca de 11 colunas. Role para baixo para ver um gráfico e código de teste.

Resposta completa:

O código para algumas das principais respostas funciona, mas há um debate subjacente aqui para a resposta "melhor", com base na aceitação do tratamento de exceções na lógica e no desempenho relacionado.

Para esclarecer isso, não acredito que exista muita orientação sobre as exceções de CAPTURA. A Microsoft tem algumas orientações sobre exceções de THROWING. Lá eles afirmam:

NÃO use exceções para o fluxo normal de controle, se possível.

A primeira nota é a clemência de "se possível". Mais importante, a descrição fornece este contexto:

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

O que isso significa é que, se você estiver escrevendo uma API que possa ser consumida por outra pessoa, permita que ela navegue em uma exceção sem uma tentativa / captura. Por exemplo, forneça um TryParse com o método Parse que lança uma exceção. Em nenhum lugar isso diz que você não deve capturar uma exceção.

Além disso, como outro usuário ressalta, as capturas sempre permitiram a filtragem por tipo e, recentemente, permitem a filtragem adicional através da cláusula when . Parece um desperdício de recursos de linguagem se não devemos usá-los.

Pode-se dizer que há ALGUM custo para uma exceção lançada, e esse custo PODE impactar o desempenho em um loop pesado. No entanto, também se pode dizer que o custo de uma exceção será insignificante em um "aplicativo conectado". O custo real foi investigado há mais de uma década: https://stackoverflow.com/a/891230/852208 Em outras palavras, o custo de uma conexão e consulta de um banco de dados provavelmente superará o de uma exceção lançada.

Tudo isso de lado, eu queria determinar qual método é realmente mais rápido. Como esperado, não há resposta concreta.

Qualquer código que passa por cima das colunas se torna mais lento conforme o número de colunas. Também pode-se dizer que qualquer código que dependa de exceções diminuirá dependendo da taxa em que a consulta não for encontrada.

Tomando as respostas de Chad Grant e Matt Hamilton, eu executei os dois métodos com até 20 colunas e até uma taxa de erro de 50% (o OP indicou que ele estava usando esse dois testes entre procs diferentes, então assumi que apenas dois) .

Aqui estão os resultados, plotados com o LinqPad: Resultados - Série 1 é Loop, 2 é Exceção

Os ziguezagues aqui são taxas de falha (coluna não encontrada) em cada contagem de colunas.

Em conjuntos de resultados mais restritos, o loop é uma boa escolha. No entanto, o método GetOrdinal / Exception não é tão sensível ao número de colunas e começa a superar o método de loop em torno de 11 colunas.

Dito isso, eu realmente não tenho um desempenho preferencial, pois 11 colunas parecem razoáveis, pois um número médio de colunas retornadas por um aplicativo inteiro. Em ambos os casos, estamos falando de frações de milissegundos aqui.

No entanto, de um aspecto de simplicidade de código e suporte a alias, eu provavelmente iria com a rota GetOrdinal.

Aqui está o teste no formato linqpad. Sinta-se livre para repassar com seu próprio 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; }


}

1
Você claramente tem algum tipo de obsessão estranha com exceções. Uma abordagem melhor seria apenas para armazenar em cache a localização coluna em uma pesquisa estática para desempenho e usar a pesquisa inteiro
Chad Grant

outro problema com o uso de exceções como fluxo de controle é que elas aparecem no criador de perfil como o número de exceções geradas quando no código sugerido elas são intencionais ... não exceções. Sem mencionar a configuração do seu depurador para quebrar as exceções lançadas. Relatando essencialmente erros que não são erros. Você não deveria estar fazendo isso.
Chad Grant

1
Também existem contadores para finalmente / s e filtros / s. Isso também é ruim? Eu chamaria isso de uma possível ressalva - a primeira real que você forneceu. Contadores são apenas informações. Eles não significam nada, a menos que correspondam a um problema de desempenho - e, neste caso, eu já mostrei o ponto em que as exceções têm MELHOR desempenho. Também indiquei que a estrutura e as bibliotecas já lançam muitas exceções. Eu tenho uma instância do visual studio lançando 60 ex / s agora. Exceções não são erros, a menos que não sejam capturadas.
precisa saber é o seguinte

Ótima análise. Eu usei seus resultados na minha nova resposta.
Yazanpro 13/11/19

1

Esse código corrige os problemas que a Levitikon teve com seu 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;
}

O motivo para obter todos esses nomes de colunas inúteis e não o nome da coluna da sua tabela ... É porque você está recebendo o nome da coluna do esquema (ou seja, os nomes das colunas da tabela Schema)

NOTA: isso parece retornar apenas o nome da primeira coluna ...

EDIT: código corrigido que retorna o nome de todas as colunas, mas você não pode usar um SqlDataReader para fazer isso

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
}

Ou em uma linha return r.GetSchemaTable().Rows.Cast<DataRow>().Select(x => (string)x["ColumnName"]).ToList();:)
nawfal 12/12/13

usar GetSchemaTable () é excessivo (em termos de alocação) apenas para encontrar um nome de coluna. Verifique a fonte github.com/microsoft/referencesource/blob/…
Chad Grant

1

Para manter seu código robusto e limpo, use uma função de extensão ú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

0

Nem fui GetSchemaTabletrabalhar até encontrar esse caminho .

Basicamente, faço isso:

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

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 é entre maiúsculas e minúsculas.


Contains () não gera exceções, esse código não faz sentido. Você só estaria capturando exceções de ponteiro nulo.
Chad Grant

0

Na sua situação específica (todos os procedimentos têm as mesmas colunas, exceto 1 que possui 1 coluna adicional), será melhor e mais rápido verificar o leitor. Propriedade FieldCount para distinguir entre eles.

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

Sei que é um post antigo, mas decidi responder para ajudar outros na mesma situação. você também pode (por motivos de desempenho) misturar esta solução com a solução de iteração da solução.


Nomeie a solução a que você está se referindo. Quais duas soluções devem ser misturadas?
Pablo Jomer 18/08/14

0

Minha classe de acesso a dados precisa ser compatível com versões anteriores; portanto, talvez eu esteja tentando acessar uma coluna em uma versão em que ela ainda não existe no banco de dados. Temos alguns conjuntos de dados bastante grandes sendo retornados, por isso não sou um grande fã de um método de extensão que precisa iterar a coleção de colunas DataReader para cada propriedade.

Eu tenho uma classe de utilitário que cria uma lista particular de colunas e, em seguida, possui um método genérico que tenta resolver um valor com base no nome da coluna e no tipo de parâmetro de saída.

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());
            }
        }
    }
}

Então eu posso chamar meu código assim

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;
    }
}

0

A chave para todo o problema está aqui :

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

Se as três linhas referenciadas (atualmente as linhas 72, 73 e 74) forem retiradas, você poderá procurar facilmente -1para determinar se a coluna não existe.

A única maneira de contornar isso e garantir o desempenho nativo é usar uma Reflectionimplementação baseada, como a seguir:

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

O método de extensão baseado em reflexão:

/// 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;
        }
    }
}


-1

E se

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

Provavelmente não seria tão eficiente em um loop


Veja a resposta da Levitikon para ver o tipo de coisa que dr.GetSchemaTable().Columnscontém - não é o que você está procurando.
Daniel Schilling

-1

Embora não exista um método exposto publicamente, existe um método na classe interna em System.Data.ProviderBase.FieldNameLookupque se SqlDataReaderbaseia.

Para acessá-lo e obter desempenho nativo, você deve usar o ILGenerator para criar um método em tempo de execução. O código a seguir lhe dará acesso direto int IndexOf(string fieldName)na System.Data.ProviderBase.FieldNameLookupclasse, além de executar a manutenção de livros que SqlDataReader.GetOrdinal()faz para que não haja efeito colateral. O código gerado espelha o existente, SqlDataReader.GetOrdinal()exceto que ele chama em FieldNameLookup.IndexOf()vez de FieldNameLookup.GetOrdinal(). O GetOrdinal()método chama a IndexOf()função e lança uma exceção se -1for retornado, portanto ignoramos esse comportamento.

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));
   }

}

1
O código interno faz quase exatamente a mesma coisa que minha resposta está fazendo, sem a necessidade dessa reflexão / delegação estranha. Ele está armazenando em cache a pesquisa por instância de objeto, o que não seria benéfico, pois no mundo real você deseja armazenar em cache os ordinais na primeira vez em que a consulta é executada e usar esse cache durante a vida útil do aplicativo, não criar um novo cache em todas as consultas.
Chad Grant

-1

esse trabalho pra mim

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.