Suchen Sie in einem SqlDataReader-Objekt nach dem Spaltennamen

212

Wie überprüfe ich, ob eine Spalte in einem SqlDataReaderObjekt vorhanden ist? In meiner Datenzugriffsschicht habe ich eine Methode erstellt, die dasselbe Objekt für mehrere Aufrufe gespeicherter Prozeduren erstellt. Eine der gespeicherten Prozeduren verfügt über eine zusätzliche Spalte, die von den anderen gespeicherten Prozeduren nicht verwendet wird. Ich möchte die Methode so ändern, dass sie für jedes Szenario geeignet ist.

Meine Bewerbung ist in C # geschrieben.

Michael Kniskern
quelle

Antworten:

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

Die Verwendung von Exceptions für die Steuerlogik wie in einigen anderen Antworten wird als schlechte Praxis angesehen und hat Leistungskosten. Es sendet auch falsch positive Ergebnisse an den Profiler für # ausgelöste Ausnahmen und Gott hilft jedem, der seinen Debugger so einstellt, dass er bei ausgelösten Ausnahmen abbricht.

GetSchemaTable () ist auch ein weiterer Vorschlag in vielen Antworten. Dies wäre keine bevorzugte Methode, um die Existenz eines Felds zu überprüfen, da es nicht in allen Versionen implementiert ist (es ist abstrakt und löst in einigen Versionen von dotnetcore eine NotSupportedException aus). GetSchemaTable ist auch in Bezug auf die Leistung übertrieben, da es eine ziemlich leistungsstarke Funktion ist, wenn Sie die Quelle überprüfen .

Das Durchlaufen der Felder kann einen kleinen Leistungseinbruch haben, wenn Sie es häufig verwenden und möglicherweise die Ergebnisse zwischenspeichern möchten.

Chad Grant
quelle
Was ist, wenn ein Alias ​​verwendet wird? Der Namensvergleich schlägt fehl.
Murphybro2
Es ist fraglich, ob die Verwendung des Ausnahmeflusses eine schlechte Praxis ist. Es wurde einmal für schlecht gehalten, weil es für andere Betreiber RELATIV teuer ist, in einer verbundenen Anwendung jedoch vernachlässigbar. Skeet hat bereits 2006 40 bis 118 Ausnahmen pro ms gemessen, abhängig von der Stapeltiefe . Stackoverflow.com/a/891230/852208 . Ohne Tests ist es außerdem möglich, dass dieser Code tatsächlich langsamer ist, da der durchschnittliche Fall die Hälfte aller Spalten überprüft (obwohl dies in einer App mit DB-Verbindung immer noch trivial ist). Ich würde diese Antwort so bearbeiten, dass sie nur den mittleren Absatz enthält, da die beiden anderen Meinungen sind.
b_levitt
3
@b_levitt es ist nicht umstritten, es ist Mist Code und Sie sollten sich nicht auf Ausnahmen für den Kontrollfluss verlassen
Chad Grant
Wie die beiden Sätze, auf die ich hingewiesen habe, ist dies eine weitere Meinung, die nicht durch eine Begründung gestützt wird, die über die Leistung in rein rechnerischer Anwendung hinausgeht. Ich wage Sie, Ihren Debugger so einzustellen, dass er bei allen Ausnahmen nicht funktioniert und nur meinen Code deaktiviert, und Sie werden sehen, wie sehr dies sogar das Framework und andere Bibliotheken bereits tun. Das Problem mit Ihrem Rat ist, dass Entwickler dazu gezwungen werden, die meisten Codes zurückzugeben stimmen zu, sind ein minderwertiges Muster: stackoverflow.com/questions/99683/… . Eine solche Methodik besteht den "Pit of Success" -Test nicht.
b_levitt
Aus Code-Sicht ist Ihre Antwort gültig. Aber Ihre Meinung, die versucht, es als überlegene Antwort auf die Antwort mit dem Versuch / Fang (der auch Aliase behandelt) zu gewichten, ist hier fehl am Platz.
b_levitt
66

Es ist viel besser, diese boolesche Funktion zu verwenden:

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

Ein Anruf - keine Ausnahmen. Es könnte intern Ausnahmen auslösen, aber ich denke nicht.

HINWEIS: In den Kommentaren unten haben wir dies herausgefunden ... der richtige Code lautet tatsächlich:

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; 
}
Jasmin
quelle
5
@ Jasmin: Ich habe zu früh gesprochen! Ihr Code sucht in der Schematabelle nach einer Spalte, nicht nach Ihrer Ergebnismenge. Sie müssen "Feld" (vorausgesetzt, "Feld" ist der Spaltenname) mit dem Wert des Felds "Spaltenname" jeder Zeile vergleichen. Brechen Sie, wenn Sie es finden, und geben Sie false zurück, wenn Sie dies nicht tun.
Steve J
4
@Steve J: Wann würde die Ergebnismenge KEINE Spalte in der GetSchemaTable haben?
Segne Yahu
1
Für alle anderen verwirrt funktioniert dies nicht. Siehe die folgende Antwort zum Abrufen und Verwenden der Spalte ColumnName aus der Schematabelle.
Jason Jackson
3
Ja, das funktioniert nicht. Wer hat es so oft bewertet ??? Es hätte mir später viel Zeit beim Debuggen gespart, wenn diese Antwort nicht hier gewesen wäre!
c00000fd
1
@ Jasmin arbeiten beide? Nicht wirklich. Bitte entfernen Sie den ersten Teil Ihrer Antwort. Ich hätte es selbst getan, aber für Ihren letzten Kommentar!
Nawfal
33

Ich denke, Ihre beste Wette ist es, GetOrdinal ("columnName") in Ihrem DataReader im Voraus aufzurufen und eine IndexOutOfRangeException abzufangen, falls die Spalte nicht vorhanden ist.

Lassen Sie uns eine Erweiterungsmethode erstellen:

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

Bearbeiten

Ok, dieser Beitrag fängt in letzter Zeit an, ein paar Abstimmungen zu erhalten, und ich kann ihn nicht löschen, da er die akzeptierte Antwort ist. Deshalb werde ich ihn aktualisieren und (ich hoffe) versuchen, die Verwendung der Ausnahmebehandlung als zu rechtfertigen Kontrollfluss.

Der andere Weg , dies zu erreichen, wie von Chad Grant - geschrieben , ist durch jedes Feld in den Datareader Schleife und macht einen Groß- und Kleinschreibung Vergleich für die Feldnamen , die Sie suchen. Dies wird sehr gut funktionieren und wird wahrheitsgemäß wahrscheinlich besser funktionieren als meine obige Methode. Sicherlich würde ich die oben beschriebene Methode niemals in einer Schleife verwenden, in der die Leistung ein Problem darstellt.

Ich kann mir eine Situation vorstellen, in der die Methode try / GetOrdinal / catch funktioniert, in der die Schleife nicht funktioniert. Es ist jedoch momentan eine völlig hypothetische Situation, daher ist es eine sehr schwache Rechtfertigung. Egal, trage mich und schau, was du denkst.

Stellen Sie sich eine Datenbank vor, mit der Sie Spalten in einer Tabelle "aliasen" können. Stellen Sie sich vor, ich könnte eine Tabelle mit einer Spalte namens "EmployeeName" definieren, ihr aber auch den Alias ​​"EmpName" geben. Wenn Sie für einen der beiden Namen eine Auswahl treffen, werden die Daten in dieser Spalte zurückgegeben. Bisher bei mir?

Stellen Sie sich nun vor, es gibt einen ADO.NET-Anbieter für diese Datenbank, und sie haben eine IDataReader-Implementierung dafür codiert, die Spaltenaliasnamen berücksichtigt.

Jetzt kann dr.GetName(i)(wie in der Antwort von Chad verwendet) nur eine einzelne Zeichenfolge zurückgegeben werden, sodass nur einer der "Aliase" in einer Spalte zurückgegeben werden muss. Sie können GetOrdinal("EmpName")jedoch die interne Implementierung der Felder dieses Anbieters verwenden, um den Alias ​​jeder Spalte auf den gesuchten Namen zu überprüfen.

In dieser hypothetischen Situation mit "Alias-Spalten" ist die try / GetOrdinal / catch-Methode die einzige Möglichkeit, um sicherzustellen, dass Sie nach jeder Variation des Spaltennamens in der Ergebnismenge suchen.

Schwach? Sicher. Aber einen Gedanken wert. Ehrlich gesagt würde ich lieber eine "offizielle" HasColumn-Methode für IDataRecord verwenden.

Matt Hamilton
quelle
15
Ausnahmen für die Steuerlogik verwenden? nein nein nein
Chad Grant
28
Es gibt eine kleine Sache, die jeder übersieht, als ich diese Frage ursprünglich gestellt habe ... Ich habe die Frage am 08.12.08 gestellt und Matt hat seine Antwort am 17.12.08 gepostet. Alle stanken nach einer Ausnahme für die Steuerlogik, boten aber erst am 01.05.09 eine solide alternative Lösung. Deshalb wurde es ursprünglich als Antwort markiert. Ich benutze diese Lösung noch heute.
Michael Kniskern
19
Dies hat nur dann einen Leistungseinbruch, wenn die Spalte nicht vorhanden war. Die anderen beschriebenen Methoden haben jedes Mal einen Leistungseinbruch und einen größeren Leistungseinbruch. Obwohl es im Allgemeinen eine schlechte Praxis ist, die Verwendung der Ausnahmebehandlung für den Kontrollfluss zu vermeiden, sollte diese Lösung nicht ausgeschlossen werden, ohne vorher zu prüfen, ob sie in Ihrem Fall funktioniert.
Nick Harrison
5
+1. Ich bin mit "Keine Ausnahme für Steuerlogik verwenden" als allgemeine Entwurfsregel einverstanden. Es bedeutet nicht "um jeden Preis vermeiden". Die Antwort ist eine sehr gut dokumentierte Problemumgehung, und wie @Nick sagt, tritt der Leistungseinbruch (falls vorhanden) nur auf, wenn die Spalte nicht vorhanden ist.
Larry
2
Die Verwendung von Ausnahmen als Steuerlogik macht das Debuggen meiner Erfahrung nach auch umständlicher. Sie müssen bei "Common Language Runtime Exceptions" die Option "Thrown" deaktivieren. Wenn Sie dann eine echte Ausnahme erhalten, kann ein Handler irgendwo und nicht in der Zeile, in der das Problem auftritt, beschädigt werden.
Cedd
30

Verwenden Sie dies in einer Zeile nach dem Abrufen von DataReader:

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

Dann,

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

Bearbeiten

Viel effizienterer Einzeiler, bei dem das Schema nicht geladen werden muss:

var exists = Enumerable.Range(0, dr.FieldCount).Any(i => string.Equals(dr.GetName(i), fieldName, StringComparison.OrdinalIgnoreCase));
Larry
quelle
Wenn Sie die Feldnamen mehrmals aufzählen / ein anderes Array zum Scannen mit einem Include zuweisen, wäre dies bei Code mit hohem Datenverkehr viel weniger leistungsfähig.
Chad Grant
@ChadGrant natürlich, deshalb ist der Linq One ​​Liner viel effizienter, da er nur eine Iteration ausführt.
Larry
18

Hier ist ein Arbeitsbeispiel für Jasmins Idee:

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

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

}
Chris Ji
quelle
1
Nur wenn Sie einen Versuch / Fang
darum
Sie können diese Idee vereinfachen mit: reader.GetSchemaTable (). Columns.Contains ("myFiled")
Lev Z
Die Verwendung von GetSchemaTable () ist übermäßig (in Bezug auf die Zuordnung), um nur einen Spaltennamen zu finden. Überprüfen Sie die Quelle github.com/microsoft/referencesource/blob/…
Chad Grant
12

das funktioniert bei mir:

bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");
Victor Labastida
quelle
Die Verwendung von GetSchemaTable () ist übermäßig (in Bezug auf die Zuordnung), um nur einen Spaltennamen zu finden. Und es ist nicht in allen Versionen von Dotnet Core implementiert. Überprüfen Sie die Quelle github.com/microsoft/referencesource/blob/…
Chad Grant
10

Folgendes ist einfach und hat für mich funktioniert:

 bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);
Paulo Lisboa
quelle
Die Verwendung von GetSchemaTable () ist übermäßig (in Bezug auf die Zuordnung), um nur einen Spaltennamen zu finden. Überprüfen Sie die Quelle github.com/microsoft/referencesource/blob/…
Chad Grant
8

Wenn Sie die Frage lesen, fragt Michael nach DataReader, nicht nach DataRecord. Machen Sie Ihre Objekte richtig.

Die Verwendung von a r.GetSchemaTable().Columns.Contains(field)in einem DataRecord funktioniert zwar, gibt jedoch BS-Spalten zurück (siehe Abbildung unten).

Verwenden Sie die folgenden Erweiterungen, um festzustellen, ob eine Datenspalte vorhanden ist UND Daten in einem DataReader enthält:

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

Verwendung:

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

Das Aufrufen r.GetSchemaTable().Columnseines DataReader gibt BS-Spalten zurück:

Aufrufen von GetSchemeTable in einem DataReader

Levitikon
quelle
Siehe Kommentare unter Matts Antwort
Nawfal
Was meinst du mit DataRecord, das zwar funktioniert , aber BS-Spalten zurückgibt ? Du meinst, es läuft (und gibt falsche Ergebnisse)?
Nawfal
2
"Machen Sie Ihre Objekte richtig." - aber IDataReaderimplementiert IDataRecord. Sie sind verschiedene Schnittstellen desselben Objekts - genau wie ICollection<T>und IEnumerable<T>sind verschiedene Schnittstellen von List<T>. IDataReaderErmöglicht das Weitergehen zum nächsten Datensatz, während das IDataRecordLesen aus dem aktuellen Datensatz ermöglicht wird. Die Methoden, die in dieser Antwort verwendet werden, stammen alle von der IDataRecordSchnittstelle. Unter stackoverflow.com/a/1357743/221708 finden Sie eine Erklärung, warum Sie den Parameter als IDataRecordvorzuziehen deklarieren .
Daniel Schilling
Upvote für das Zeigen, warum r.GetSchemaTable().Columnseine absolut falsche Antwort auf diese Frage ist.
Daniel Schilling
GetName () wird von der IDataRecord-Schnittstelle an IDataReader vererbt. Das Targeting der Basisschnittstelle ist der richtige Code.
Chad Grant
7

Ich habe für Visual Basic-Benutzer geschrieben:

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

Ich denke, das ist mächtiger und die Verwendung ist:

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

Hier ist eine einzeilige Linq-Version der akzeptierten Antwort:

Enumerable.Range(0, reader.FieldCount).Any(i => reader.GetName(i) == "COLUMN_NAME_GOES_HERE")
Clement
quelle
Groß- und Kleinschreibung beachten ... warum?
Chad Grant
4

Hier die Lösung von Jasmine in einer Zeile ... (noch eine, tho simple!):

reader.GetSchemaTable().Select("ColumnName='MyCol'").Length > 0;
Spaark
quelle
Die Verwendung von GetSchemaTable () ist übermäßig (in Bezug auf die Zuordnung), um nur einen Spaltennamen zu finden. Überprüfen Sie die Quelle github.com/microsoft/referencesource/blob/…
Chad Grant
@ChadGrant Möglich. Ich denke, man muss mit Bedacht wählen, abhängig vom Kontext und der Häufigkeit, mit der dies verwendet werden muss ...
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
quelle
3

TLDR:

Viele Antworten mit Behauptungen über Leistung und schlechte Praxis, deshalb kläre ich das hier.

Die Ausnahmeroute ist schneller für eine höhere Anzahl zurückgegebener Spalten, die Schleifenroute ist schneller für eine niedrigere Anzahl von Spalten und der Überkreuzungspunkt liegt bei etwa 11 Spalten. Scrollen Sie nach unten, um ein Diagramm und einen Testcode anzuzeigen.

Vollständige Antwort:

Der Code für einige der Top-Antworten funktioniert, aber es gibt hier eine grundlegende Debatte für die "bessere" Antwort, die auf der Akzeptanz der Ausnahmebehandlung in der Logik und der damit verbundenen Leistung basiert.

Um das zu klären, glaube ich nicht, dass es viele Hinweise zu CATCHING-Ausnahmen gibt. Microsoft hat einige Anleitungen zum Werfen von Ausnahmen. Dort heißt es:

Verwenden Sie nach Möglichkeit KEINE Ausnahmen für den normalen Kontrollfluss.

Die erste Anmerkung ist die Nachsicht von "wenn möglich". Noch wichtiger ist, dass die Beschreibung diesen Kontext angibt:

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

Dies bedeutet, dass Sie beim Schreiben einer API, die möglicherweise von einer anderen Person verwendet wird, die Möglichkeit erhalten, eine Ausnahme ohne Versuch / Fang zu navigieren. Stellen Sie beispielsweise TryParse mit Ihrer ausnahmeauslösenden Parse-Methode bereit. Nirgendwo heißt es jedoch, dass Sie keine Ausnahme abfangen sollten.

Wie ein anderer Benutzer betont, haben Fänge außerdem immer das Filtern nach Typ und in jüngster Zeit das weitere Filtern über die when-Klausel ermöglicht . Dies scheint eine Verschwendung von Sprachfunktionen zu sein, wenn wir sie nicht verwenden sollen.

Es kann gesagt werden, dass es einige Kosten für eine ausgelöste Ausnahme gibt, und diese Kosten können die Leistung in einer schweren Schleife beeinflussen. Es kann jedoch auch gesagt werden, dass die Kosten einer Ausnahme in einer "verbundenen Anwendung" vernachlässigbar sein werden. Die tatsächlichen Kosten wurden vor über einem Jahrzehnt untersucht: https://stackoverflow.com/a/891230/852208 Mit anderen Worten, die Kosten für eine Verbindung und Abfrage einer Datenbank werden wahrscheinlich die Kosten einer ausgelösten Ausnahme in den Schatten stellen.

Abgesehen davon wollte ich herausfinden, welche Methode wirklich schneller ist. Wie erwartet gibt es keine konkrete Antwort.

Jeder Code, der die Spalten durchläuft, wird langsamer, wenn die Anzahl der Spalten vorhanden ist. Es kann auch gesagt werden, dass jeder Code, der auf Ausnahmen beruht, abhängig von der Rate, in der die Abfrage nicht gefunden wird, langsamer wird.

Ich nahm die Antworten von Chad Grant und Matt Hamilton und führte beide Methoden mit bis zu 20 Spalten und einer Fehlerrate von bis zu 50% aus (das OP gab an, dass er diese beiden Tests zwischen verschiedenen Prozessen verwendete, also nahm ich nur zwei an). .

Hier sind die mit LinqPad gezeichneten Ergebnisse: Ergebnisse - Serie 1 ist Schleife, 2 ist Ausnahme

Die Zickzacklinien hier sind Fehlerraten (Spalte nicht gefunden) innerhalb jeder Spaltenanzahl.

Bei engeren Ergebnismengen ist das Schleifen eine gute Wahl. Die GetOrdinal / Exception-Methode reagiert jedoch bei weitem nicht so empfindlich auf die Anzahl der Spalten und beginnt, die Schleifenmethode um 11 Spalten herum zu übertreffen.

Das heißt, ich habe keine Präferenz für die Leistung, da 11 Spalten als durchschnittliche Anzahl von Spalten, die über eine gesamte Anwendung zurückgegeben werden, vernünftig klingen. In beiden Fällen handelt es sich hier um Bruchteile einer Millisekunde.

Unter dem Aspekt der Code-Einfachheit und der Alias-Unterstützung würde ich mich wahrscheinlich für die GetOrdinal-Route entscheiden.

Hier ist der Test in Linqpad-Form. Fühlen Sie sich frei, mit Ihrer eigenen Methode neu zu posten:

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
quelle
1
Sie haben eindeutig eine seltsame Besessenheit mit Ausnahmen. Ein besserer Ansatz wäre nur, die Spaltenposition in einer statischen Suche für die Leistung zwischenzuspeichern und die ganzzahlige Suche zu verwenden
Chad Grant
Ein weiteres Problem bei der Verwendung von Ausnahmen als Kontrollfluss besteht darin, dass sie im Profiler als Anzahl der ausgelösten Ausnahmen angezeigt werden, wenn sie in Ihrem vorgeschlagenen Code beabsichtigt sind ... keine Ausnahmen. Ganz zu schweigen davon, dass Sie Ihren Debugger so einstellen, dass er bei ausgelösten Ausnahmen nicht funktioniert. Im Wesentlichen Fehler melden, die keine Fehler sind. Du solltest das nicht tun.
Chad Grant
1
Es gibt auch Zähler für finallys / sec und Filter / sec. Sind die auch schlecht? Ich würde es eine mögliche Einschränkung nennen - die erste echte, die Sie bereitgestellt haben. Zähler sind nur Informationen. Sie bedeuten nichts, es sei denn, sie entsprechen einem Leistungsproblem - und in diesem Fall habe ich bereits den Punkt gezeigt, an dem Ausnahmen eine BESSERE Leistung haben. Ich habe auch darauf hingewiesen, dass das Framework und die Bibliotheken bereits viele Ausnahmen auslösen. Ich habe gerade eine Instanz von Visual Studio, die 60 Ex / s wirft. Ausnahmen sind keine Fehler, es sei denn, sie werden nicht erfasst.
b_levitt
Tolle Analyse. Ich habe die Ergebnisse in meiner neuen Antwort verwendet.
Yazanpro
1

Dieser Code behebt die Probleme, die Levitikon mit seinem Code hatte: (angepasst von: [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;
}

Der Grund dafür, dass all diese nutzlosen Spaltennamen und nicht der Name der Spalte aus Ihrer Tabelle abgerufen werden, liegt darin, dass Sie den Namen der Schemaspalte erhalten (dh die Spaltennamen für die Schematabelle).

HINWEIS: Dies scheint nur den Namen der ersten Spalte zurückzugeben ...

BEARBEITEN: Der Code wurde korrigiert, der den Namen aller Spalten zurückgibt. Sie können jedoch keinen SqlDataReader verwenden, um dies zu tun

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
quelle
Oder in einer Zeile return r.GetSchemaTable().Rows.Cast<DataRow>().Select(x => (string)x["ColumnName"]).ToList();:)
Nawfal
Die Verwendung von GetSchemaTable () ist übermäßig (in Bezug auf die Zuordnung), um nur einen Spaltennamen zu finden. Überprüfen Sie die Quelle github.com/microsoft/referencesource/blob/…
Chad Grant
1

Verwenden Sie eine einzelne Erweiterungsfunktion wie folgt, um Ihren Code robust und sauber zu halten:

    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.
quelle
0

Ich habe mich auch nicht GetSchemaTablean die Arbeit gemacht, bis ich diesen Weg gefunden habe .

Grundsätzlich mache ich das:

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
quelle
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 unterscheidet übrigens zwischen Groß- und Kleinschreibung.

RBAFF79
quelle
Contains () löst keine Ausnahmen aus, dieser Code ist sinnlos. Sie würden nur Nullzeigerausnahmen abfangen.
Chad Grant
0

In Ihrer speziellen Situation (alle Prozeduren haben die gleichen Spalten mit Ausnahme von 1 mit einer zusätzlichen Spalte von 1) ist es besser und schneller, den Leser zu überprüfen. FieldCount-Eigenschaft zur Unterscheidung.

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

Ich weiß, dass es ein alter Beitrag ist, aber ich habe mich entschlossen zu antworten, um anderen in der gleichen Situation zu helfen. Sie können diese Lösung auch (aus Leistungsgründen) mit der Lösungsiterationslösung mischen.

pkrzemo
quelle
Bitte nennen Sie die Lösung, auf die Sie sich beziehen. Welche zwei Lösungen sollten gemischt werden?
Pablo Jomer
0

Meine Datenzugriffsklasse muss abwärtskompatibel sein, daher versuche ich möglicherweise, auf eine Spalte in einer Version zuzugreifen, in der sie noch nicht in der Datenbank vorhanden ist. Wir haben einige ziemlich große Datenmengen zurückgegeben, daher bin ich kein großer Fan einer Erweiterungsmethode, die die DataReader-Spaltensammlung für jede Eigenschaft iterieren muss.

Ich habe eine Dienstprogrammklasse, die eine private Liste von Spalten erstellt und dann eine generische Methode hat, die versucht, einen Wert basierend auf einem Spaltennamen und einem Ausgabeparametertyp aufzulösen.

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

Dann kann ich einfach meinen Code so aufrufen

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
quelle
0

Der Schlüssel zum ganzen Problem ist hier :

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

Wenn die drei referenzierten Zeilen (derzeit die Zeilen 72, 73 und 74) entfernt werden, können Sie leicht überprüfen -1, ob die Spalte nicht vorhanden ist.

Die einzige Möglichkeit, dies zu umgehen und gleichzeitig die native Leistung sicherzustellen, besteht darin, eine Reflectionbasierte Implementierung wie die folgende zu verwenden:

Verwendung:

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

Die auf Reflexion basierende Erweiterungsmethode:

/// 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
quelle
-1

Wie wäre es mit

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

In einer Schleife wäre es wahrscheinlich nicht so effizient

Skadoosh
quelle
Sehen Sie sich Levitikons Antwort an, um zu sehen, was darin dr.GetSchemaTable().Columnsenthalten ist - es ist nicht das, wonach Sie suchen.
Daniel Schilling
-1

Obwohl es keine öffentlich zugängliche Methode gibt, existiert in der internen Klasse eine Methode, auf die sich System.Data.ProviderBase.FieldNameLookupdie Methode SqlDataReaderstützt.

Um darauf zuzugreifen und native Leistung zu erhalten, müssen Sie den ILGenerator verwenden, um zur Laufzeit eine Methode zu erstellen. Mit dem folgenden Code erhalten Sie direkten Zugriff auf int IndexOf(string fieldName)die System.Data.ProviderBase.FieldNameLookupKlasse und können die Buchführung durchführen, SqlDataReader.GetOrdinal()damit keine Nebenwirkungen auftreten. Der generierte Code spiegelt den vorhandenen Code wider, SqlDataReader.GetOrdinal()außer dass er FieldNameLookup.IndexOf()anstelle von aufruft FieldNameLookup.GetOrdinal(). Die GetOrdinal()Methode ruft die IndexOf()Funktion auf und löst eine Ausnahme aus, wenn sie -1zurückgegeben wird. Daher umgehen wir dieses Verhalten.

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
quelle
1
Der interne Code macht fast genau das, was meine Antwort tut, ohne dass diese seltsame Reflexion / dieser seltsame Delegierte erforderlich ist. Das Zwischenspeichern der Suche pro Objektinstanz ist nicht vorteilhaft, da Sie in der realen Welt die Ordnungszahlen beim ersten Ausführen der Abfrage zwischenspeichern und diesen Cache über die gesamte Lebensdauer der App verwenden möchten, und nicht bei jeder Abfrage einen neuen Cache erstellen möchten.
Chad Grant
-1

diese Arbeit für mich

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
quelle