Sprawdź nazwę kolumny w obiekcie SqlDataReader

212

Jak sprawdzić, czy kolumna istnieje w pliku SqlDataReader obiekcie? W mojej warstwie dostępu do danych utworzyłem metodę, która buduje ten sam obiekt dla wielu wywołań procedur składowanych. Jedna z procedur przechowywanych ma dodatkową kolumnę, która nie jest używana przez inne procedury przechowywane. Chcę zmodyfikować metodę, aby dostosować ją do każdego scenariusza.

Moja aplikacja jest napisana w języku C #.

Michael Kniskern
źródło

Odpowiedzi:

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

Używanie Exceptions do logiki sterowania, jak w niektórych innych odpowiedziach, jest uważane za złą praktykę i wiąże się z kosztami wydajności. Wysyła również fałszywe alarmy do profilera zgłoszonych # wyjątków i niech Bóg pomoże każdemu, kto ustawi swojego debuggera na złamanie zgłoszonych wyjątków.

GetSchemaTable () to także kolejna sugestia w wielu odpowiedziach. Nie byłby to preferowany sposób sprawdzania istnienia pola, ponieważ nie jest on implementowany we wszystkich wersjach (jest abstrakcyjny i zgłasza NotSupportedException w niektórych wersjach dotnetcore). GetSchemaTable ma również zbyt dużą wydajność, ponieważ jest to dość ciężka funkcja, jeśli sprawdzisz źródło .

Pętlowanie przez pola może mieć niewielki wpływ na wydajność, jeśli często go używasz i możesz rozważyć buforowanie wyników.

Chad Grant
źródło
Co się stanie, jeśli użyty zostanie alias? Porównanie nazw nie powiedzie się.
Murphybro2
Można dyskutować, że stosowanie przepływu wyjątków jest złą praktyką. Kiedyś uważano go za zły, ponieważ jest względnie drogi dla innych operatorów, ale w przypadku podłączonej aplikacji jest nieistotny. Skeet mierzona 40-118 wyjątki za ms w zależności od głębokości stosu całą drogę z powrotem w 2006 roku stackoverflow.com/a/891230/852208 . Co więcej, bez testowania, możliwe jest, że ten kod jest rzeczywiście wolniejszy, a średnia wielkość liter sprawdza połowę wszystkich kolumn (choć nadal jest trywialna w aplikacji podłączonej do bazy danych). Zredagowałbym tę odpowiedź, aby uwzględnić tylko środkowy akapit, ponieważ pozostałe dwa są opiniami.
b_levitt
3
@b_levitt nie podlega dyskusji, jest to bzdura i nie powinieneś polegać na wyjątkach przepływu kontroli
Chad Grant
Podobnie jak dwa zdania, które wskazałem, jest to kolejna opinia, która nie jest poparta żadnym uzasadnieniem poza wydajnością w aplikacji czysto obliczeniowej. Wyzywam cię, byś ustawił debugger na łamanie wszystkich wyjątków i wyłączał tylko mój kod, a zobaczysz, jak wiele nawet framework i inne biblioteki już to robią. Problem z twoją radą polega na tym, że popycha programistów do zwracania kodów, które najbardziej zgadzają się gorszy wzór: stackoverflow.com/questions/99683/... . Taka metodologia kończy się niepowodzeniem testu „otchłani sukcesu”.
b_levitt
Z punktu widzenia kodu twoja odpowiedź jest prawidłowa. Ale twoja opinia, która próbuje traktować ją jako doskonałą odpowiedź na odpowiedź za pomocą try / catch (która obsługuje także aliasy), jest tutaj nie na miejscu.
b_levitt,
66

O wiele lepiej jest użyć tej funkcji boolowskiej:

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

Jedno połączenie - bez wyjątków. Może powodować wewnętrzne wyjątki, ale nie sądzę.

UWAGA: W komentarzach poniżej zorientowaliśmy się ... Właściwy kod to tak naprawdę:

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; 
}
Jaśmin
źródło
5
@Jasmine: Rozmawiałem zbyt wcześnie! Twój kod sprawdza kolumnę w tabeli schematów, a nie zestaw wyników. Musisz porównać „pole” (zakładając, że „pole” jest nazwą kolumny) z wartością pola „Nazwa kolumny” każdego wiersza. Złam, gdy go znajdziesz, zwróć fałsz, jeśli nie.
Steve J
4
@ Steve J: Kiedy zestaw wyników NIE miałby kolumny w tabeli GetSchemaTable?
Pobłogosław Yahu
1
Dla kogokolwiek innego zdezorientowanego, TO NIE DZIAŁA. Zobacz poniższą odpowiedź na temat pobierania wiersza ColumnName z tabeli schematu i korzystania z niego.
Jason Jackson
3
Tak, to NIE DZIAŁA. Kto tyle razy go głosował ??? Oszczędziłoby mi to dużo czasu na debugowanie, gdyby nie było tej odpowiedzi!
c00000fd 15.04.13
1
@Jasmine oboje pracują? Nie całkiem. Uprzejmie usuń pierwszą część swojej odpowiedzi. Zrobiłbym sam, ale dla twojego ostatniego komentarza!
nawfal
33

Myślę, że najlepszym rozwiązaniem jest wywołanie GetOrdinal („columnName”) na swoim DataReaderze z góry i wyłapanie IndexOutOfRangeException na wypadek, gdyby kolumna nie była obecna.

W rzeczywistości stwórzmy metodę rozszerzenia:

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

Edytować

Ok, ten post zaczyna ostatnio zbierać kilka głosów negatywnych i nie mogę go usunąć, ponieważ jest to zaakceptowana odpowiedź, więc zaktualizuję go i (mam nadzieję) spróbuję uzasadnić użycie obsługi wyjątków jako kontrola przepływu.

Innym sposobem osiągnięcia tego, jak napisał Chad Grant , jest przejście przez każde pole w czytniku danych i wykonanie rozróżniania wielkości liter dla szukanej nazwy pola. To zadziała naprawdę dobrze i prawdę mówiąc prawdopodobnie będzie działało lepiej niż moja metoda powyżej. Z pewnością nigdy nie użyłbym powyższej metody w pętli, w której występowanie stanowiło problem.

Mogę wymyślić jedną sytuację, w której metoda try / GetOrdinal / catch będzie działać tam, gdzie pętla nie działa. Jest to jednak obecnie całkowicie hipotetyczna sytuacja, więc jest to bardzo słabe uzasadnienie. Niezależnie od tego, znoś mnie i zobacz, co myślisz.

Wyobraź sobie bazę danych, która pozwoliła na „alias” kolumn w tabeli. Wyobraź sobie, że mógłbym zdefiniować tabelę z kolumną o nazwie „Nazwa pracownika”, ale także nadać jej alias „Nazwa Emp”, a wybranie jednej z nazw zwróciłoby dane w tej kolumnie. Do tej pory ze mną?

Wyobraź sobie teraz, że istnieje dostawca ADO.NET dla tej bazy danych i zakodowali dla niej implementację IDataReader, która uwzględnia aliasy kolumn.

Teraz dr.GetName(i)(użyte w odpowiedzi Chada) może zwrócić tylko jeden ciąg, więc musi zwrócić tylko jeden z „aliasów” w kolumnie. Jednak GetOrdinal("EmpName")przydałby wewnętrzną realizację polach tego dostawcy w celu sprawdzenia każdej kolumny alias dla nazwy, której szukasz.

W tej hipotetycznej sytuacji „kolumny z aliasem” metoda try / GetOrdinal / catch byłaby jedynym sposobem, aby upewnić się, że sprawdzane są wszystkie odmiany nazwy kolumny w zestawie wyników.

Słaby? Pewnie. Ale warto pomyśleć. Szczerze mówiąc wolałbym raczej „oficjalną” metodę HasColumn na IDataRecord.

Matt Hamilton
źródło
15
używasz wyjątków dla logiki sterowania? nie nie nie
Chad Grant
28
Jest jedna drobna rzecz, którą wszyscy przeoczają, kiedy pierwotnie opublikowałem to pytanie ... Zadałem pytanie 08.08.08, a Matt opublikował odpowiedź 17.12.08. Wszyscy śmiali się z wychwytywania wyjątku dla logiki sterowania, ale nie stanowili solidnego alternatywnego rozwiązania do 5/1/09. Dlatego pierwotnie został oznaczony jako odpowiedź. Nadal używam tego rozwiązania dzisiaj.
Michael Kniskern,
19
Będzie to miało wpływ na wydajność tylko wtedy, gdy nie będzie tam kolumny. Inne opisane metody będą miały za każdym razem działanie związane z wydajnością i działanie związane z większą wydajnością. Chociaż generalnie złą praktyką jest unikanie obsługi wyjątków dla przepływu kontroli, to rozwiązanie nie powinno być wykluczone bez uprzedniego rozważenia, czy działa w twoim przypadku.
Nick Harrison
5
+1. Nie przeszkadza mi „Nie używaj wyjątku dla logiki sterowania” jako ogólna zasada projektowania. Nie oznacza to „unikaj tego za wszelką cenę”. Odpowiedź jest bardzo dobrze udokumentowanym obejściem, a jak mówi @Nick, wzrost wydajności (jeśli w ogóle ...) występuje tylko wtedy, gdy kolumna nie istnieje.
Larry,
2
Używanie wyjątków jako logiki sterowania sprawia, że ​​debugowanie jest bardziej kłopotliwe z mojego doświadczenia. Musisz odznaczyć opcję „Zgłoszony” w „Wyjątkach środowiska uruchomieniowego wspólnego języka”, a następnie, gdy pojawi się prawdziwy wyjątek, może on gdzieś włamać się do programu obsługi, a nie w linii, która ma problem.
cedd
30

W jednym wierszu użyj tego po pobraniu DataReadera:

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

Następnie,

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

Edytować

Znacznie bardziej wydajna jedna linijka, która nie wymaga ładowania schematu:

var exists = Enumerable.Range(0, dr.FieldCount).Any(i => string.Equals(dr.GetName(i), fieldName, StringComparison.OrdinalIgnoreCase));
Larry
źródło
Wyliczasz nazwy pól wiele razy / przydzielasz inną tablicę do skanowania za pomocą zawiera, byłoby to znacznie mniej wydajne w kodzie o dużym ruchu.
Chad Grant,
@ChadGrant oczywiście, dlatego jedna linijka Linq jest znacznie bardziej wydajna, ponieważ wykonuje tylko jedną iterację.
Larry,
18

Oto działająca próbka pomysłu Jasmina:

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

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

}
Chris Ji
źródło
1
Tylko jeśli
obejmiesz
Możesz uprościć ten pomysł za pomocą: reader.GetSchemaTable (). Columns.Contains („myFiled”)
Lev Z
użycie GetSchemaTable () jest nadmierne (jeśli chodzi o alokację), aby znaleźć tylko nazwę kolumny. Sprawdź źródło github.com/microsoft/referencesource/blob/…
Chad Grant
12

to działa dla mnie:

bool hasColumnName = reader.GetSchemaTable().AsEnumerable().Any(c => c["ColumnName"] == "YOUR_COLUMN_NAME");
Victor Labastida
źródło
użycie GetSchemaTable () jest nadmierne (jeśli chodzi o alokację), aby znaleźć nazwę kolumny. I nie jest zaimplementowany we wszystkich wersjach rdzenia dotnet. Sprawdź źródło github.com/microsoft/referencesource/blob/…
Chad Grant
10

Poniższe jest proste i działało dla mnie:

 bool hasMyColumn = (reader.GetSchemaTable().Select("ColumnName = 'MyColumnName'").Count() == 1);
Paulo Lisboa
źródło
użycie GetSchemaTable () jest nadmierne (jeśli chodzi o alokację), aby znaleźć nazwę kolumny. Sprawdź źródło github.com/microsoft/referencesource/blob/…
Chad Grant
8

Jeśli przeczytasz pytanie, Michael zapytał o DataReader, a nie ludzi DataRecord. Zrób swoje przedmioty we właściwy sposób.

Używać r.GetSchemaTable().Columns.Contains(field) na DataRecord działa, ale zwraca kolumny BS (patrz zrzut ekranu poniżej).

Aby sprawdzić, czy kolumna danych istnieje ORAZ zawiera dane w czytniku danych, użyj następujących rozszerzeń:

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

Stosowanie:

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

Wywołanie r.GetSchemaTable().Columnsw DataReader zwraca kolumny BS:

Wywoływanie GetSchemeTable w czytniku danych

Levitikon
źródło
patrz komentarze pod odpowiedzią Matts
nawfal
Co rozumiesz przez DataRecord działa , ale zwraca kolumny BS ? Masz na myśli, że działa (i daje złe wyniki)?
nawfal,
2
„Popraw swoje obiekty”. - ale IDataReaderimplementuje IDataRecord. Są to różne interfejsy tego samego obiektu - podobnie jak ICollection<T>i IEnumerable<T>są różne interfejsy List<T>. IDataReaderumożliwia przejście do następnego rekordu, jednocześnie IDataRecordumożliwiając odczyt z bieżącego rekordu. Wszystkie metody użyte w tej odpowiedzi pochodzą z IDataRecordinterfejsu. Zobacz stackoverflow.com/a/1357743/221708, aby uzyskać wyjaśnienie, dlaczego warto zadeklarować parametr jako IDataRecordpreferowany.
Daniel Schilling
Głosuj za pokazanie, dlaczego r.GetSchemaTable().Columnsodpowiedź na to pytanie jest absolutnie zła.
Daniel Schilling
GetName () jest dziedziczony z interfejsu IDataRecord do IDataReader. Kierowanie na interfejs podstawowy jest poprawnym kodem.
Chad Grant,
7

Napisałem dla użytkowników 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

Myślę, że jest to bardziej wydajne, a użycie jest następujące:

If HasColumnAndValue(reader, "ID_USER") Then
    Me.UserID = reader.GetDecimal(reader.GetOrdinal("ID_USER")).ToString()
End If
HoLyVieR
źródło
4

Oto jedna liniowa wersja linq akceptowanej odpowiedzi:

Enumerable.Range(0, reader.FieldCount).Any(i => reader.GetName(i) == "COLUMN_NAME_GOES_HERE")
Łaskawy
źródło
rozróżnianie wielkości liter ... dlaczego?
Chad Grant
4

Oto rozwiązanie od Jasmine w jednym wierszu ... (jeszcze jedno, proste!):

reader.GetSchemaTable().Select("ColumnName='MyCol'").Length > 0;
spaark
źródło
użycie GetSchemaTable () jest nadmierne (jeśli chodzi o alokację), aby znaleźć tylko nazwę kolumny. Sprawdź źródło github.com/microsoft/referencesource/blob/…
Chad Grant
@ChadGrant Możliwe.
Wydaje
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
źródło
3

TLDR:

Wiele odpowiedzi z twierdzeniami na temat wydajności i złych praktyk, więc wyjaśniam to tutaj.

Trasa wyjątku jest szybsza dla większej liczby zwracanych kolumn, trasa pętli jest szybsza dla mniejszej liczby kolumn, a punkt podziału wynosi około 11 kolumn. Przewiń w dół, aby zobaczyć wykres i kod testowy.

Pełna odpowiedź:

Kod niektórych najważniejszych odpowiedzi działa, ale tutaj jest debata na temat „lepszej” odpowiedzi opartej na akceptacji obsługi wyjątków w logice i związanej z nią wydajności.

Aby to wyjaśnić, nie wierzę, że istnieje wiele wskazówek dotyczących wyjątków ŁOWIENIE. Microsoft ma pewne wskazówki dotyczące wyjątków WYPROWADZANIE. Tam stwierdzają:

Jeśli to możliwe, NIE używaj wyjątków dla normalnego przepływu kontroli.

Pierwsza uwaga to łagodność „jeśli to możliwe”. Co ważniejsze, opis podaje następujący kontekst:

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

Oznacza to, że jeśli piszesz interfejs API, który może być używany przez kogoś innego, daj mu możliwość poruszania się po wyjątku bez próbowania / chwytania. Na przykład podaj TryParse ze swoją metodą analizy wyjątków Parse. Nigdzie nie mówi to jednak, że nie powinieneś wychwytywać wyjątku.

Ponadto, jak zauważa inny użytkownik, połowy zawsze pozwalały na filtrowanie według typu, a nieco później na dalsze filtrowanie poprzez klauzulę when . Wydaje się to stratą funkcji językowych, jeśli nie powinniśmy ich używać.

Można powiedzieć, że istnieje NIEKTÓRY koszt rzuconego wyjątku, który może MNIE wpłynąć na wydajność w ciężkiej pętli. Można jednak powiedzieć, że koszt wyjątku będzie znikomy w „połączonej aplikacji”. Rzeczywisty koszt zbadano ponad dziesięć lat temu: https://stackoverflow.com/a/891230/852208 Innymi słowy, koszt połączenia i zapytania do bazy danych może być niższy niż w przypadku zgłoszonego wyjątku.

Poza tym chciałem ustalić, która metoda naprawdę jest szybsza. Zgodnie z oczekiwaniami nie ma konkretnej odpowiedzi.

Każdy kod, który zapętla się nad kolumnami, staje się wolniejszy w miarę istnienia liczby kolumn. Można również powiedzieć, że każdy kod, który opiera się na wyjątkach, zwolni w zależności od częstotliwości, w której zapytanie nie zostanie znalezione.

Biorąc odpowiedzi zarówno Chada Granta, jak i Matta Hamiltona, zastosowałem obie metody z maksymalnie 20 kolumnami i do 50% poziomu błędu (OP wskazał, że używa tego testu między różnymi procesami, więc założyłem, że tylko dwa) .

Oto wyniki przedstawione za pomocą LinqPad: Wyniki - Seria 1 to Pętla, 2 to Wyjątek

Zygzaki tutaj są wskaźnikami błędów (nie znaleziono kolumny) w obrębie każdej liczby kolumn.

W porównaniu z węższymi zestawami wyników, zapętlenie jest dobrym wyborem. Jednak metoda GetOrdinal / Exception nie jest tak wrażliwa na liczbę kolumn i zaczyna przewyższać metodę zapętlania około 11 kolumn.

To powiedziawszy, tak naprawdę nie mam preferencji pod względem wydajności, ponieważ 11 kolumn brzmi rozsądnie, ponieważ średnia liczba kolumn zwrócona w całej aplikacji. W obu przypadkach mówimy tutaj o ułamkach milisekundy.

Jednak z punktu widzenia prostoty kodu i obsługi aliasów prawdopodobnie wybrałbym trasę GetOrdinal.

Oto test w formie linqpad. Zapraszam do repost z własną metodą:

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
źródło
1
Wyraźnie masz jakąś dziwną obsesję na punkcie wyjątków. Lepszym podejściem byłoby po prostu buforowanie lokalizacji kolumny w statycznym wyszukiwaniu wydajności i użycie wyszukiwania liczb całkowitych
Chad Grant
innym problemem związanym ze stosowaniem wyjątków jako przepływu sterowania jest to, że są one wyświetlane w module profilującym jako liczba wyjątków zgłoszonych, gdy w sugerowanym kodzie są one zamierzone ... nie wyjątki. Nie wspominając już o ustawianiu debuggera łamania wyjątków. Zasadniczo zgłaszanie błędów, które nie są błędami. Nie powinieneś tego robić.
Chad Grant,
1
Istnieją również liczniki finałów / s i filtrów / s. Czy te też są złe? Nazwałbym to możliwym zastrzeżeniem - pierwszym prawdziwym, które dostarczyłeś. Licznik to tylko informacja. Nic nie znaczą, chyba że odpowiadają problemowi z wydajnością - w tym przypadku już pokazałem punkt, w którym wyjątki mają LEPSZĄ wydajność. Wskazałem również, że framework i biblioteki już rzucają wiele wyjątków. Mam teraz przykład studia wizualnego rzucającego 60 eks / s. Wyjątki nie są błędami, chyba że są wyłapane.
b_levitt,
Świetna analiza. Użyłem jej wyników w mojej nowej odpowiedzi.
yazanpro,
1

Ten kod rozwiązuje problemy, które miał Levitikon ze swoim kodem: (zaadaptowano z: [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;
}

Powodem, dla którego wszystkie te bezużyteczne nazwy kolumn są uzyskiwane, a nie nazwa kolumny z tabeli ... jest to, że otrzymujesz nazwę kolumny schematu (tj. Nazwy kolumn dla tabeli schematów)

UWAGA: wydaje się, że to zwraca tylko nazwę pierwszej kolumny ...

EDYCJA: poprawiony kod, który zwraca nazwę wszystkich kolumn, ale nie można do tego użyć SqlDataReader

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
źródło
Lub w jednym wierszu return r.GetSchemaTable().Rows.Cast<DataRow>().Select(x => (string)x["ColumnName"]).ToList();:)
nawfal
użycie GetSchemaTable () jest nadmierne (jeśli chodzi o alokację), aby znaleźć tylko nazwę kolumny. Sprawdź źródło github.com/microsoft/referencesource/blob/…
Chad Grant
1

Aby Twój kod był niezawodny i czysty, użyj jednej funkcji rozszerzenia, takiej jak ta:

    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.
źródło
0

Nie zabrałem się też GetSchemaTabledo pracy, dopóki nie znalazłem w ten sposób .

Zasadniczo robię to:

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
źródło
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 jest rozróżniana wielkość liter btw.

RBAFF79
źródło
Zawiera () nie zgłasza wyjątków, ten kod jest bezcelowy. Łapałbyś tylko wyjątki wskaźnika zerowego.
Chad Grant,
0

W twojej konkretnej sytuacji (wszystkie procedury mają te same kolumny, z wyjątkiem 1, która ma dodatkową 1 kolumnę), lepiej i szybciej będzie sprawdzać czytnik. Właściwość FieldCount, aby je rozróżnić.

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

Wiem, że to stary post, ale postanowiłem odpowiedzieć, aby pomóc innym w tej samej sytuacji. możesz także (ze względu na wydajność) mieszać to rozwiązanie z iteracyjnym rozwiązaniem.

pkrzemo
źródło
Podaj nazwę rozwiązania, którego dotyczysz. Które dwa rozwiązania należy mieszać?
Pablo Jomer
0

Moja klasa dostępu do danych musi być kompatybilna z poprzednimi wersjami, więc być może próbuję uzyskać dostęp do kolumny w wydaniu, w którym jeszcze jej nie ma w bazie danych. Zwracane są pewne dość duże zestawy danych, więc nie jestem wielkim fanem metody rozszerzenia, która musi iterować kolekcję kolumn DataReader dla każdej właściwości.

Mam klasę narzędzi, która tworzy prywatną listę kolumn, a następnie ma ogólną metodę, która próbuje rozwiązać wartość na podstawie nazwy kolumny i typu parametru wyjściowego.

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

Następnie mogę tak po prostu wywołać mój kod

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
źródło
0

Klucz do całego problemu jest tutaj :

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

Jeśli odwołane trzy linie (obecnie linie 72, 73 i 74) zostaną usunięte, możesz łatwo sprawdzić -1, czy kolumna nie istnieje.

Jedynym sposobem na obejście tego przy jednoczesnym zapewnieniu wydajności natywnej jest użycie Reflectionimplementacji opartej na następujących czynnościach:

Zastosowania:

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

Metoda rozszerzenia oparta na odbiciu:

/// 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
źródło
-1

Co powiesz na

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

Prawdopodobnie nie byłoby tak wydajne w pętli

Skadoosh
źródło
Zobacz odpowiedź Levitikon, aby zobaczyć, co dr.GetSchemaTable().Columnszawiera - to nie jest to, czego szukasz.
Daniel Schilling
-1

Chociaż nie ma metody ujawnionej publicznie, metoda istnieje w klasie wewnętrznej, System.Data.ProviderBase.FieldNameLookupktóraSqlDataReader opiera.

Aby uzyskać do niego dostęp i uzyskać natywną wydajność, musisz użyć ILGenerator, aby utworzyć metodę w czasie wykonywania. Poniższy kod daje bezpośredni dostęp do int IndexOf(string fieldName)w System.Data.ProviderBase.FieldNameLookupklasie, a także wykonać księgowości, że SqlDataReader.GetOrdinal()robi tak, że nie ma efekt uboczny. Wygenerowany kod odzwierciedla istniejący, SqlDataReader.GetOrdinal()z wyjątkiem tego, że wywołuje FieldNameLookup.IndexOf()zamiast FieldNameLookup.GetOrdinal(). GetOrdinal()Metoda zwraca się do IndexOf()funkcji i zgłasza wyjątek, jeśli -1jest zwracany, więc obejścia tego zachowania.

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
źródło
1
Wewnętrzny kod robi dokładnie to samo, co robi moja odpowiedź, bez potrzeby tego dziwnego refleksji / delegowania. Buforuje wyszukiwanie według instancji obiektu, co nie byłoby korzystne, ponieważ w prawdziwym świecie chcesz buforować porządki przy pierwszym uruchomieniu zapytania i używać tej pamięci przez cały czas istnienia aplikacji, a nie budować nową pamięć podręczną dla każdego zapytania.
Chad Grant,
-1

ta praca dla mnie

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
źródło