Controlla il nome della colonna in un oggetto SqlDataReader


212

Come posso verificare se esiste una colonna in a SqlDataReader oggetto? Nel mio livello di accesso ai dati, ho creato un metodo che crea lo stesso oggetto per più chiamate di procedure memorizzate. Una delle stored procedure ha una colonna aggiuntiva che non viene utilizzata dalle altre stored procedure. Voglio modificare il metodo per adattarlo a ogni scenario.

La mia domanda è scritta in C #.

Risposte:


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

L'uso di Exceptions come logica di controllo come in alcune altre risposte è considerato una cattiva pratica e ha costi di prestazione. Invia inoltre falsi positivi al profiler delle # eccezioni generate e dio aiuta chiunque a impostare il proprio debugger per interrompere le eccezioni generate.

GetSchemaTable () è anche un altro suggerimento in molte risposte. Questo non sarebbe un modo preferito per verificare l'esistenza di un campo in quanto non è implementato in tutte le versioni (è astratto e genera NotSupportedException in alcune versioni di dotnetcore). GetSchemaTable è anche eccessivo in termini di prestazioni in quanto è una funzione piuttosto pesante se si controlla la fonte .

Il ciclo attraverso i campi può avere un piccolo impatto sulle prestazioni se lo usi molto e potresti prendere in considerazione la memorizzazione nella cache dei risultati.


Cosa succede se viene utilizzato un alias? Il confronto dei nomi fallirà.
Murphybro2,

È discutibile che l'utilizzo del flusso di eccezioni sia una cattiva pratica. Una volta è stato pensato male perché è RELATIVAMENTE costoso per altri operatori, ma trascurabile in un'applicazione connessa. Lo skeet ha misurato 40-118 eccezioni per ms a seconda della profondità dello stack nel 2006. stackoverflow.com/a/891230/852208 . Inoltre, senza test, è possibile che questo codice sia effettivamente più lento con il caso medio che controlla metà di tutte le colonne (anche se è ancora banale in un'app connessa al db). Modificherei questa risposta per includere solo il paragrafo centrale poiché le altre due sono opinioni.
b_levitt,

3
@b_levitt non è discutibile, è un codice merda e non dovresti fare affidamento su eccezioni per il flusso di controllo
Chad Grant

Come le due frasi che ho indicato, questa è un'altra opinione che non è supportata da alcuna logica al di là delle prestazioni in un'applicazione puramente computazionale. Ti sfido a impostare il tuo debugger su tutte le eccezioni e disabilitare solo il mio codice e vedrai quanto anche il framework e le altre librerie lo stanno già facendo. Il problema con il tuo consiglio è che spinge gli sviluppatori a restituire i codici che più d'accordo sono un modello inferiore: stackoverflow.com/questions/99683/… . Tale metodologia non supera il test della "fossa del successo".
b_levitt,

Dal punto di vista del codice, la tua risposta è valida. Ma la tua opinione cercando di valutarla come una risposta superiore alla risposta con il try / catch (che gestisce anche gli alias) è fuori posto qui.
b_levitt,

66

È molto meglio usare questa funzione booleana:

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

Una chiamata: nessuna eccezione. Potrebbe generare eccezioni internamente, ma io non la penso così.

NOTA: Nei commenti seguenti, l'abbiamo capito ... il codice corretto è in realtà questo:

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: ho parlato troppo presto! Il codice controlla una colonna nella tabella dello schema, non il set di risultati. Devi confrontare "campo" (supponendo che "campo" sia il nome della colonna) con il valore del campo "Nome colonna" di ogni riga. Rompi quando lo trovi, restituisci false se non lo fai.
Steve J,

4
@Steve J: Quando il gruppo di risultati NON dovrebbe avere una colonna in GetSchemaTable?
Benedici Yahu il

1
Per chiunque altro confuso, QUESTO NON FUNZIONA. Vedere la risposta di seguito sul recupero della riga ColumnName dalla tabella dello schema e sull'utilizzo.
Jason Jackson,

3
Sì, questo NON FUNZIONA. Chi l'ha votato così tante volte ??? Mi farebbe risparmiare un sacco di tempo per il debug in seguito se questa risposta non fosse qui!
c00000fd,

1
@Jasmine funzionano entrambi? Non proprio. Rimuovi la prima parte della tua risposta. Mi sarei fatto da solo, ma per il tuo ultimo commento!
nawfal,

33

Penso che la tua scommessa migliore sia chiamare GetOrdinal ("columnName") sul tuo DataReader in anticipo e catturare un IndexOutOfRangeException nel caso in cui la colonna non sia presente.

In effetti, facciamo un metodo di estensione:

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

modificare

Ok, questo post sta iniziando a raccogliere alcuni voti negativi ultimamente e non posso eliminarlo perché è la risposta accettata, quindi lo aggiornerò e (spero) cercherò di giustificare l'uso della gestione delle eccezioni come flusso di controllo.

L'altro modo per raggiungere questo obiettivo, come pubblicato da Chad Grant , è quello di scorrere ogni campo nel DataReader ed effettuare un confronto senza distinzione tra maiuscole e minuscole per il nome del campo che stai cercando. Funzionerà davvero bene e, in verità, probabilmente funzionerà meglio del mio metodo sopra. Certamente non avrei mai usato il metodo sopra in un ciclo in cui performace era un problema.

Posso pensare a una situazione in cui il metodo try / GetOrdinal / catch funzionerà dove il ciclo non funziona. È, tuttavia, una situazione completamente ipotetica in questo momento, quindi è una giustificazione molto fragile. Indipendentemente da ciò, abbi pazienza con me e vedi cosa ne pensi.

Immagina un database che ti ha permesso di "alias" le colonne all'interno di una tabella. Immagina di poter definire una tabella con una colonna chiamata "EmployeeName" ma anche di dargli un alias di "EmpName", e fare una selezione per entrambi i nomi restituirebbe i dati in quella colonna. Con me finora?

Ora immagina che ci sia un provider ADO.NET per quel database, e hanno codificato un'implementazione IDataReader che tiene conto degli alias di colonna.

Ora, dr.GetName(i)(come usato nella risposta di Chad) può restituire solo una singola stringa, quindi deve restituire solo uno degli "alias" su una colonna. Però,GetOrdinal("EmpName") possibile utilizzare l'implementazione interna dei campi di questo provider per controllare l'alias di ogni colonna per il nome che stai cercando.

In questa ipotetica situazione di "colonne con alias", il metodo try / GetOrdinal / catch sarebbe l'unico modo per essere sicuri di verificare ogni variazione del nome di una colonna nel gruppo di risultati.

Fragile? Sicuro. Ma vale la pena pensarci. Onestamente preferirei piuttosto un metodo HasColumn "ufficiale" su IDataRecord.


15
usando le eccezioni per la logica di controllo? no no no
Chad Grant

28
C'è una piccola cosa che tutti trascurano quando ho originariamente pubblicato questa domanda ... Ho fatto la domanda il 08/12/08 e Matt ha pubblicato la sua risposta il 17/12/08. Tutti hanno fatto una puzza di catturare un'eccezione per la logica di controllo, ma non hanno fornito una solida soluzione alternativa fino al 01/05/09. Ecco perché è stato originariamente contrassegnato come risposta. Sto ancora usando questa soluzione oggi.
Michael Kniskern,

19
Ciò avrà un impatto sulle prestazioni solo se la colonna non era presente. Gli altri metodi descritti avranno un hit delle prestazioni e un hit delle prestazioni più ampio, ogni volta. Sebbene sia generalmente una cattiva pratica evitare di utilizzare la gestione delle eccezioni per il flusso di controllo, questa soluzione non dovrebbe essere esclusa senza prima considerare se funziona nel tuo caso.
Nick Harrison,

5
+1. Sono d'accordo con "Non usare l'eccezione per la logica di controllo" come regola generale di progettazione. Non significa "evitarlo a tutti i costi". La risposta è una soluzione molto ben documentata e, come dice @Nick, l'hit di performance (se presente ..) si verifica solo quando la colonna non esiste.
Larry,

2
L'uso delle eccezioni come logica di controllo rende anche il debug più complicato nella mia esperienza. Devi deselezionare "Gettato" su "Common Language Runtime Exceptions" e quindi quando ottieni una vera eccezione potrebbe rompersi in un gestore da qualche parte e non sulla linea che presenta il problema.
cedd,

30

In una riga, utilizzalo dopo il recupero del tuo DataReader:

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

Poi,

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

modificare

One-liner molto più efficiente che non richiede di caricare lo schema:

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

Stai enumerando i nomi dei campi più volte / allocando un altro array per eseguire la scansione con un contenuto, questo sarebbe molto meno performante nel codice di traffico elevato.
Chad Grant,

@ChadGrant ovviamente, ecco perché il Linq one liner è molto più efficiente poiché esegue solo una iterazione.
Larry,

18

Ecco un esempio funzionante dell'idea di Jasmin:

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

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

}

1
Solo se ci provi /
catturalo

Puoi semplificare questa idea con: reader.GetSchemaTable (). Columns.Contains ("myFiled")
Lev Z

l'utilizzo di GetSchemaTable () è eccessivo (per quanto riguarda l'allocazione) solo per trovare un nome di colonna. Scopri la fonte github.com/microsoft/referencesource/blob/…
Chad Grant

12

questo funziona per me:

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

l'utilizzo di GetSchemaTable () è eccessivo (per quanto riguarda l'allocazione) solo per trovare un nome di colonna. E non è implementato in tutte le versioni di dotnet core. Scopri la fonte github.com/microsoft/referencesource/blob/…
Chad Grant


8

Se leggi la domanda, Michael ha chiesto di DataReader, non di DataRecord. Ottieni i tuoi oggetti giusti.

Usare un r.GetSchemaTable().Columns.Contains(field) su un DataRecord funziona, ma restituisce colonne BS (vedere la schermata seguente).

Per vedere se esiste una colonna di dati E contiene dati in un DataReader, utilizzare le seguenti estensioni:

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

La chiamata r.GetSchemaTable().Columnsa un DataReader restituisce colonne BS:

Chiamata GetSchemeTable in un DataReader


vedere i commenti nella risposta di Matts
nawfal

Cosa intendi con DataRecord funziona , ma restituisce colonne BS ? Vuoi dire che funziona (e dà risultati sbagliati)?
nawfal,

2
"Ottieni i tuoi oggetti giusti." - ma IDataReaderimplementa IDataRecord. Sono interfacce diverse dello stesso oggetto - proprio come ICollection<T>e IEnumerable<T>sono interfacce diverse di List<T>. IDataReaderconsente di passare al record successivo, mentre IDataRecordconsente di leggere dal record corrente. I metodi utilizzati in questa risposta provengono tutti IDataRecorddall'interfaccia. Vedere stackoverflow.com/a/1357743/221708 per una spiegazione del motivo per cui IDataRecordè preferibile dichiarare il parametro .
Daniel Schilling,

Voto per mostrare perché r.GetSchemaTable().Columnsè una risposta assolutamente sbagliata a questa domanda.
Daniel Schilling,

GetName () viene ereditato dall'interfaccia IDataRecord in IDataReader. Il targeting dell'interfaccia di base è il codice corretto.
Chad Grant,

7

Ho scritto per gli utenti di 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

Penso che questo sia più potente e l'uso sia:

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

4

Ecco una versione linq di una riga della risposta accettata:

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

confronto case sensitive ... perché?
Chad Grant,

4

Ecco la soluzione di Jasmine in una riga ... (un'altra, anche se semplice!):

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

l'utilizzo di GetSchemaTable () è eccessivo (per quanto riguarda l'allocazione) solo per trovare un nome di colonna. Scopri la fonte github.com/microsoft/referencesource/blob/…
Chad Grant

@ChadGrant Possible. Immagino che uno debba scegliere saggiamente a seconda del contesto e della frequenza che è necessario usare questo ...
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:

Molte risposte con affermazioni su prestazioni e cattive pratiche, quindi chiarisco che qui.

Il percorso di eccezione è più veloce per un numero maggiore di colonne restituite, il percorso del ciclo è più veloce per un numero inferiore di colonne e il punto di crossover è di circa 11 colonne. Scorri verso il basso per visualizzare un grafico e un codice di prova.

Risposta completa:

Il codice per alcune delle risposte migliori funziona, ma qui c'è un dibattito sottostante per la risposta "migliore" basata sull'accettazione della gestione delle eccezioni nella logica e delle prestazioni correlate.

Per chiarire ciò, non credo che ci siano molte indicazioni riguardo alle eccezioni di CATCHING. Microsoft ha alcune indicazioni per quanto riguarda le eccezioni di LANCIO. Lì dichiarano:

NON utilizzare eccezioni per il normale flusso di controllo, se possibile.

La prima nota è la clemenza di "se possibile". Ancora più importante, la descrizione fornisce questo contesto:

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

Ciò significa che se stai scrivendo un'API che potrebbe essere utilizzata da qualcun altro, dai loro la possibilità di navigare in un'eccezione senza provare / catturare. Ad esempio, fornire un TryParse con il metodo Parse che genera eccezioni. Da nessuna parte questo dice che non dovresti prendere un'eccezione.

Inoltre, come sottolineato da un altro utente, le catture hanno sempre consentito il filtraggio per tipo e in qualche modo recentemente consentono un ulteriore filtraggio tramite la clausola when . Sembra uno spreco di funzionalità linguistiche se non dovremmo usarle.

Si può dire che vi è ALCUNO costo per un'eccezione generata e che può influire sulle prestazioni in un ciclo pesante. Tuttavia, si può anche affermare che il costo di un'eccezione sarà trascurabile in una "applicazione connessa". Il costo effettivo è stato esaminato oltre un decennio fa: https://stackoverflow.com/a/891230/852208 In altre parole, è probabile che il costo di una connessione e una query di un database riducano quello di un'eccezione generata.

A parte questo, volevo determinare quale metodo fosse veramente più veloce. Come previsto, non esiste una risposta concreta.

Qualsiasi codice che scorre sopra le colonne diventa più lento man mano che esiste il numero di colonne. Si può anche dire che qualsiasi codice che si basa su eccezioni rallenterà a seconda della velocità con cui la query non viene trovata.

Prendendo le risposte di Chad Grant e Matt Hamilton, ho eseguito entrambi i metodi con un massimo di 20 colonne e un tasso di errore fino al 50% (l'OP ha indicato che stava usando questi due test tra processi diversi, quindi ne ho ipotizzati solo due) .

Ecco i risultati, tracciati con LinqPad: Risultati - La serie 1 è Loop, 2 è eccezione

Gli zigzag qui sono i tassi di errore (colonna non trovata) all'interno di ogni conteggio delle colonne.

Su insiemi di risultati più ristretti, il looping è una buona scelta. Tuttavia, il metodo GetOrdinal / Exception non è altrettanto sensibile al numero di colonne e inizia a sovraperformare il metodo di ciclo attorno a 11 colonne.

Detto questo, in realtà non ho una preferenza prestazionale in quanto 11 colonne sembrano ragionevoli poiché un numero medio di colonne restituite su un'intera applicazione. In entrambi i casi stiamo parlando di frazioni di millisecondo qui.

Tuttavia, dal punto di vista della semplicità del codice e del supporto alias, probabilmente andrei con il percorso GetOrdinal.

Ecco il test in formato linqpad. Sentiti libero di ripubblicare con il tuo metodo:

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
Hai chiaramente una sorta di strana ossessione per le eccezioni. Un approccio migliore sarebbe solo memorizzare nella cache la posizione della colonna in una ricerca statica per le prestazioni e utilizzare la ricerca per intero
Chad Grant,

un altro problema con l'utilizzo delle eccezioni come flusso di controllo è che vengono visualizzate nel profiler come numero di eccezioni generate quando nel codice suggerito sono intenzionali ... non eccezioni. Per non parlare dell'impostazione del debugger per interrompere le eccezioni generate. Sostanzialmente riportare errori che non sono errori. Non dovresti farlo.
Chad Grant,

1
Ci sono anche contatori per finallys / sec e filtri / sec. Anche quelli sono cattivi? Lo definirei un possibile avvertimento, il primo vero che hai fornito. I contatori sono solo informazioni. Non significano nulla a meno che non corrispondano a un problema di prestazioni - e in questo caso ho già mostrato il punto in cui le eccezioni hanno prestazioni MIGLIORI. Ho anche indicato che il framework e le librerie generano già molte eccezioni. Ho un'istanza di Visual Studio che lancia 60 ex / s in questo momento. Le eccezioni non sono errori a meno che non vengano rilevate.
b_levitt,

Ottima analisi. Ho usato i suoi risultati nella mia nuova risposta.
yazanpro,

1

Questo codice corregge i problemi che Levitikon aveva con il loro codice: (adattato da: [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;
}

Il motivo per ottenere tutti quei nomi di colonne inutili e non il nome della colonna dalla tabella ... È perché stai ottenendo il nome della colonna dello schema (ovvero i nomi delle colonne per la tabella Schema)

NOTA: questo sembra restituire solo il nome della prima colonna ...

EDIT: codice corretto che restituisce il nome di tutte le colonne, ma non è possibile utilizzare un SqlDataReader per farlo

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
}

O in una riga return r.GetSchemaTable().Rows.Cast<DataRow>().Select(x => (string)x["ColumnName"]).ToList();:)
nawfal

l'utilizzo di GetSchemaTable () è eccessivo (per quanto riguarda l'allocazione) solo per trovare un nome di colonna. Scopri la fonte github.com/microsoft/referencesource/blob/…
Chad Grant

1

Per mantenere il codice solido e pulito, utilizzare una singola funzione di estensione, come questa:

    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

Né sono riuscito GetSchemaTablea lavorare, fino a quando non l'ho trovato in questo modo .

Fondamentalmente faccio questo:

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 è maiuscole e minuscole.


Contains () non genera eccezioni, questo codice è inutile. Si verificherebbero solo eccezioni di puntatore null.
Chad Grant,

0

Nella tua situazione particolare (tutte le procedure hanno le stesse colonne tranne 1 che ha 1 colonna aggiuntiva), sarà meglio e più veloce controllare il lettore. Proprietà FieldCount per distinguerli.

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

So che è un vecchio post, ma ho deciso di rispondere per aiutare gli altri nella stessa situazione. puoi anche (per motivi di prestazioni) mescolare questa soluzione con la soluzione iterante.


Indica la soluzione a cui ti riferisci. Quali due soluzioni dovrebbero essere mescolate?
Pablo Jomer,

0

La mia classe di accesso ai dati deve essere retrocompatibile, quindi potrei provare ad accedere a una colonna in una versione in cui non esiste ancora nel database. Abbiamo alcuni set di dati piuttosto grandi che vengono restituiti, quindi non sono un grande fan di un metodo di estensione che deve iterare la raccolta di colonne DataReader per ogni proprietà.

Ho una classe di utilità che crea un elenco privato di colonne e quindi ha un metodo generico che tenta di risolvere un valore basato sul nome di una colonna e sul tipo di parametro di output.

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

Quindi posso semplicemente chiamare il mio codice in questo modo

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

La chiave per l'intero problema è qui :

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

Se le tre linee di riferimento (attualmente le linee 72, 73 e 74) vengono rimosse, è possibile verificare facilmente la presenza di -1 determinare se la colonna non esiste.

L'unico modo per ovviare a questo mentre garantendo prestazioni native è utilizzare a Reflection un'implementazione basata, come la seguente:

usings:

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

Il metodo di estensione basato su 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;
        }
    }
}


-1

Che ne dite di

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

Probabilmente non sarebbe così efficiente in un ciclo


Vedi la risposta di Levitikon per vedere il tipo di cose che dr.GetSchemaTable().Columnscontiene - non è quello che stai cercando.
Daniel Schilling,

-1

Sebbene non esista un metodo esposto pubblicamente, esiste un metodo nella classe interna System.Data.ProviderBase.FieldNameLookupcheSqlDataReader si basa.

Per accedervi e ottenere prestazioni native, è necessario utilizzare ILGenerator per creare un metodo in fase di esecuzione. Il seguente codice vi darà accesso diretto int IndexOf(string fieldName)in System.Data.ProviderBase.FieldNameLookupclasse e di eseguire la tenuta libro che SqlDataReader.GetOrdinal()fa in modo che non v'è alcun effetto collaterale. Il codice generato rispecchia quello esistente SqlDataReader.GetOrdinal()tranne per il fatto che chiama FieldNameLookup.IndexOf()invece di FieldNameLookup.GetOrdinal(). Il GetOrdinal()metodo chiama la IndexOf()funzione e genera un'eccezione se -1viene restituito, quindi ignoriamo quel 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
Il codice interno fa quasi la stessa cosa della mia risposta, senza la necessità di questo strano riflesso / delegato. Sta memorizzando nella cache la ricerca per istanza di oggetto che non sarebbe utile poiché nel mondo reale si desidera memorizzare nella cache gli ordinali la prima volta che viene eseguita la query e utilizzare quella cache per tutta la vita dell'app, non creare una nuova cache su ogni query.
Chad Grant

-1

questo lavoro per me

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.