Czy połączenie z bazą danych zostanie zamknięte, jeśli otrzymamy wiersz modułu danych i nie odczytamy wszystkich rekordów?

15

Rozumiejąc, jak yielddziała słowo kluczowe, natknąłem się na link1 i link2 na StackOverflow, który zaleca korzystanie z yield returniteracji nad DataReaderem i odpowiada to również moim potrzebom. Ale zastanawiam się, co się stanie, jeśli użyję, yield returnjak pokazano poniżej i jeśli nie wykonam iteracji przez cały DataReader, czy połączenie DB pozostanie otwarte na zawsze?

IEnumerable<IDataRecord> GetRecords()
{
    SqlConnection myConnection = new SqlConnection(@"...");
    SqlCommand myCommand = new SqlCommand(@"...", myConnection);
    myCommand.CommandType = System.Data.CommandType.Text;
    myConnection.Open();
    myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
    try
       {               
          while (myReader.Read())
          {
            yield return myReader;          
          }
        }
    finally
        {
            myReader.Close();
        }
}


void AnotherMethod()
{
    foreach(var rec in GetRecords())
    {
       i++;
       System.Console.WriteLine(rec.GetString(1));
       if (i == 5)
         break;
  }
}

Próbowałem tego samego przykładu w przykładowej aplikacji konsolowej i zauważyłem podczas debugowania, że ​​ostatecznie blok GetRecords()nie jest wykonywany. Jak mogę wtedy zapewnić zamknięcie połączenia DB? Czy istnieje lepszy sposób niż użycie yieldsłowa kluczowego? Próbuję zaprojektować niestandardową klasę, która będzie odpowiedzialna za wykonywanie wybranych instrukcji SQL i procedur składowanych w bazie danych i zwróci wynik. Ale nie chcę zwracać DataReader do dzwoniącego. Chcę również upewnić się, że połączenie zostanie zamknięte we wszystkich scenariuszach.

Edytuj Zmieniono odpowiedź na odpowiedź Bena, ponieważ niepoprawne jest oczekiwanie, że osoby wywołujące metodę będą używać tej metody poprawnie, a w odniesieniu do połączenia z DB będzie ona droższa, jeśli metoda zostanie wywołana wiele razy bez powodu.

Dzięki Jakob i Ben za szczegółowe wyjaśnienia.

GawdePrasad
źródło
Z tego, co widzę, nie otwieracie połączenia w pierwszej kolejności
paparazzo
@Frisbee Edytowano :)
GawdePrasad

Odpowiedzi:

12

Tak, napotkasz opisany problem: dopóki nie zakończysz iteracji wyniku, połączenie pozostanie otwarte. Są dwa ogólne podejścia, które mogę wymyślić, aby sobie z tym poradzić:

Pchaj, nie ciągnij

Obecnie zwracasz IEnumerable<IDataRecord>strukturę danych, z której możesz pobrać. Zamiast tego możesz zmienić metodę, aby wypchnąć jej wyniki. Najprostszym sposobem byłoby przekazanie polecenia, Action<IDataRecord>które jest wywoływane przy każdej iteracji:

void GetRecords(Action<IDataRecord> callback)
{
    // ...
      while (myReader.Read())
      {
        callback(myReader);
      }
}

Pamiętaj, że biorąc pod uwagę, że masz do czynienia z kolekcją elementów, IObservable/ IObservermoże być nieco bardziej odpowiednią strukturą danych, ale jeśli nie jest to potrzebne, prosta Actionjest znacznie prostsza .

Chętnie oceniaj

Alternatywą jest upewnienie się, że iteracja całkowicie się zakończyła przed powrotem.

Zwykle możesz to zrobić, po prostu umieszczając wyniki na liście, a następnie zwracając je, ale w tym przypadku istnieje dodatkowa komplikacja każdego elementu będącego tym samym odwołaniem do czytelnika. Potrzebujesz więc czegoś, co wyodrębni wynik z czytnika:

IEnumerable<T> GetRecords<T>(Func<IDataRecord,T> extractor)
{
    // ...
     var result = new List<T>();
     try
     {
       while (myReader.Read())
       {
         result.Add(extractor(myReader));         
       }
     }
     finally
     {
         myReader.Close();
     }
     return result;
}
Ben Aaronson
źródło
Korzystam z podejścia, które nazwałeś Eagerly Evaluate. Czy będzie to narzut pamięci, jeśli moduł danych mojego SQLCommand zwróci wiele danych wyjściowych (takich jak myReader.NextResult ()) i utworzę listę? Czy wysyłanie modułu danych do dzwoniącego [osobiście nie wolę tego] będzie znacznie wydajniejsze? [ale połączenie będzie otwarte dłużej]. To, co dokładnie mnie tutaj myli, to kompromis między utrzymywaniem połączenia dłużej na dłużej niż tworzeniem listy. Możesz bezpiecznie założyć, że na mojej stronie internetowej, która łączy się z DB, będzie ponad 500 osób.
GawdePrasad
1
@GawdePrasad To zależy od tego, co dzwoniący zrobiłby z czytnikiem. Jeśli po prostu umieściłby przedmioty w samej kolekcji, nie byłoby znacznego obciążenia. Gdyby wykonał operację, która nie wymaga przechowywania całej masy wartości w pamięci, oznacza to narzut pamięci. Jednak jeśli nie przechowujesz bardzo dużych ilości, prawdopodobnie nie jest to narzut, na który powinieneś się zwrócić. Tak czy inaczej, nie powinno być znacznego narzutu czasu.
Ben Aaronson
W jaki sposób podejście „push, Don't pull” rozwiązuje problem „dopóki nie skończysz iteracji nad wynikiem, utrzymasz połączenie otwarte”? Połączenie jest nadal otwarte, dopóki nie skończysz iteracji, nawet przy takim podejściu, prawda?
BornToCode,
1
@BornToCode Zobacz pytanie: „jeśli użyję zwrotu z zysków, jak pokazano poniżej i jeśli nie wykonam iteracji przez cały DataReader, połączenie DB pozostanie otwarte na zawsze”. Problem polega na tym, że zwracasz IEnumerable, a każdy dzwoniący, który go obsługuje, musi wiedzieć o tym specjalnym gotcha: „Jeśli nie skończysz iteracji nade mną, będę mieć otwarte połączenie”. W metodzie push odbierasz tę odpowiedzialność od dzwoniącego - nie ma on opcji częściowej iteracji. Gdy kontrola zostanie zwrócona, połączenie zostanie zamknięte.
Ben Aaronson
1
To doskonała odpowiedź. Podoba mi się podejście push, ponieważ jeśli masz duże zapytanie, które przedstawiasz w formie stronicowania, możesz wcześniej przerwać czytanie, aby przyspieszyć, przekazując Func <> zamiast Action <>; wydaje się to czystsze niż włączenie funkcji GetRecords również do stronicowania.
Whelkaholism
10

Twój finallyblok zawsze będzie wykonywany.

Podczas korzystania yield returnz kompilatora utworzy nową klasę zagnieżdżoną do implementacji automatu stanów.

Ta klasa będzie zawierać cały kod z finallybloków jako osobne metody. Będzie śledził finallybloki, które należy wykonać w zależności od stanu. Wszystkie niezbędne finallybloki zostaną wykonane Disposemetodą.

Zgodnie ze specyfikacją języka C # foreach (V v in x) embedded-statementjest rozszerzony do

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            V v = (V)(T)e.Current;
            embedded - statement
        }
    }
    finally {
         // Dispose e
    }
}

więc moduł wyliczający zostanie usunięty, nawet jeśli wyjdziesz z pętli za pomocą breaklub return.

Aby uzyskać więcej informacji na temat implementacji iteratora, przeczytaj ten artykuł Jon Skeet

Edytować

Problem z takim podejściem polega na tym, że polegasz na klientach swojej klasy, aby prawidłowo używać metody. Mogliby na przykład pobrać moduł wyliczający bezpośrednio i iterować go w whilepętli bez usuwania go.

Powinieneś rozważyć jedno z rozwiązań sugerowanych przez @BenAaronson.

Jakub Lortz
źródło
1
To jest bardzo zawodne. Na przykład: var records = GetRecords(); var first = records.First(); var count = records.Count()faktycznie wykona tę metodę dwukrotnie, za każdym razem otwierając i zamykając połączenie i czytnik. Dwukrotne powtórzenie tego samego robi to samo. Możesz również przekazać wynik przez długi czas, zanim zacznie się iteracja itd. Nic w podpisie metody nie sugeruje tego rodzaju zachowania
Ben Aaronson
2
@BenAaronson W mojej odpowiedzi skupiłem się na konkretnym pytaniu dotyczącym bieżącego wdrożenia. Jeśli spojrzysz na cały problem, zgadzam się, że o wiele lepiej jest użyć sposobu, który zasugerowałeś w swojej odpowiedzi.
Jakub Lortz
1
@JububLortz Fair Fair
Ben Aaronson
2
@EsbenSkovPedersen Zwykle, gdy próbujesz zrobić coś odpornego na idioty, tracisz trochę funkcjonalności. Musisz to zrównoważyć. Tutaj nie tracimy wiele, chętnie ładując wyniki. W każdym razie największym problemem dla mnie jest nazewnictwo. Kiedy widzę metodę o nazwie GetRecordsreturn IEnumerable, oczekuję, że załaduje ona rekordy do pamięci. Z drugiej strony, jeśli Recordsbyłaby to właściwość , wolałbym raczej założyć, że jest to jakaś abstrakcja nad bazą danych.
Jakub
1
@EsbenSkovPedersen Cóż, zobacz moje komentarze do odpowiedzi nvoigt. Ogólnie nie mam problemu z metodami zwracającymi IEnumerable, który wykona znaczną pracę po iteracji, ale w tym przypadku nie wydaje się, aby pasował do implikacji podpisu metody
Ben Aaronson
5

Niezależnie od określonego yieldzachowania, kod zawiera ścieżki wykonania, które nie będą poprawnie pozbywały się zasobów. Co się stanie, jeśli druga linia zgłosi wyjątek lub trzecią? A nawet twój czwarty? Będziesz potrzebować bardzo złożonego łańcucha try / wreszcie lub możesz użyć usingbloków.

IEnumerable<IDataRecord> GetRecords()
{
    using(var connection = new SqlConnection(@"..."))
    {
        connection.Open();

        using(var command = new SqlCommand(@"...", connection);
        {
            using(var reader = command.ExecuteReader())
            {
               while(reader.Read())
               {
                   // your code here.
                   yield return reader;
               }
            }
        }
    }
}

Ludzie zauważyli, że nie jest to intuicyjne, że osoby wywołujące twoją metodę mogą nie wiedzieć, że dwukrotne wyliczenie wyniku wywoła metodę dwukrotnie. Cóż, pech. Tak działa ten język. To jest sygnał, żeIEnumerable<T> wysyła. Istnieje powód, dla którego nie zwraca List<T>lub T[]. Ludzie, którzy nie wiedzą o tym, muszą się kształcić, a nie pracować.

Visual Studio ma funkcję o nazwie statyczna analiza kodu. Możesz go użyć, aby sprawdzić, czy prawidłowo zutylizowałeś zasoby.

nvoigt
źródło
Język pozwala na powtarzanie IEnumerableróżnych czynności, takich jak zgłaszanie całkowicie niepowiązanych wyjątków lub zapisywanie plików na dysku o pojemności 5 GB. Tylko dlatego, że język na to pozwala, nie oznacza, że ​​dopuszczalne jest zwrócenie IEnumerabletego, co robi to z metody, która nie daje żadnych wskazówek, że tak się stanie.
Ben Aaronson,
1
Zwrócenie an IEnumerable<T> oznacza , że dwukrotne policzenie spowoduje dwa połączenia. Otrzymasz nawet pomocne ostrzeżenia w swoich narzędziach, jeśli wyliczysz wyliczenie wiele razy. Punkt dotyczący zgłaszania wyjątków jest równie ważny bez względu na typ zwrotu. Jeśli ta metoda zwróci a List<T>, wyrzuci te same wyjątki.
nvoigt
Rozumiem twoją tezę i do pewnego stopnia się zgadzam - jeśli używasz metody, która daje ci IEnumerableopróżnienie. Ale myślę też, że jako twórca metod jesteś odpowiedzialny za przestrzeganie tego, co sugeruje twój podpis. Wywoływana jest metoda GetRecords, więc kiedy powróci, można się spodziewać, że uzyskała rekordy . Gdyby został nazwany GiveMeSomethingICanPullRecordsFrom(lub bardziej realistycznie GetRecordSourcelub podobnie), leniwy IEnumerablebyłby bardziej do przyjęcia.
Ben Aaronson,
@BenAaronson Zgadzam się, że na przykład w File, ReadLinesi ReadAllLinesmożna je łatwo rozdzielić i to dobrze. (Chociaż jest to bardziej spowodowane faktem, że przeciążenie metody nie może różnić się tylko typem zwrotu). Może ReadRecords i ReadAllRecords pasują tutaj również jako schemat nazewnictwa.
nvoigt
2

To, co zrobiłeś, wydaje się złe, ale zadziała dobrze.

Powinieneś użyć IEnumerator <> , ponieważ dziedziczy on także IDisposable .

Ale ponieważ używasz foreach, kompilator nadal używa IDisposable do generowania IEnumerator <> dla foreach.

w rzeczywistości Foreach zawiera wiele rzeczy wewnętrznych.

Sprawdź /programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-use

Avlin
źródło