datareader 행을 생성하고 모든 레코드를 읽지 않으면 데이터베이스 연결이 닫히나요?


15

yield키워드의 작동 방식을 이해하는 동안 StackOverflow 에서 link1link2 를 발견 yield return하여 DataReader를 반복하는 동안 사용을 옹호하며 내 필요에 적합합니다. 그러나 yield return아래 그림과 같이 사용하고 전체 DataReader를 반복하지 않으면 DB 연결이 영원히 열려 있습니까?

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

샘플 콘솔 앱에서 동일한 예제를 시도하고 finally 블록이 GetRecords()실행되지 않는 것을 디버깅하는 동안 알았습니다 . DB 연결이 닫히도록하려면 어떻게해야합니까? yield키워드를 사용하는 것보다 더 좋은 방법이 있습니까? DB에서 select SQL 및 저장 프로 시저를 실행하고 결과를 반환하는 사용자 정의 클래스를 설계하려고합니다. 그러나 DataReader를 호출자에게 반환하고 싶지 않습니다. 또한 모든 시나리오에서 연결이 닫히고 싶습니다.

편집 Ben의 답변에 대한 답변이 변경되었으므로 메소드 호출자가 메소드를 올바르게 사용할 것으로 예상하고 DB 연결과 관련하여 아무 이유없이 메소드를 여러 번 호출하면 더 비쌉니다.

자세한 설명은 Jakob과 Ben에게 감사드립니다.


내가 본 것에서 당신은 처음부터 연결을 열지 않습니다
paparazzo

@Frisbee 편집 :)
GawdePrasad

답변:


12

예, 설명하는 문제에 직면하게됩니다. 결과에 대한 반복이 완료 될 때까지 연결을 열린 상태로 유지합니다. 이 문제를 처리 할 수있는 두 가지 일반적인 접근 방식이 있습니다.

밀지 마

현재 IEnumerable<IDataRecord>가져올 수있는 데이터 구조를 반환하고 있습니다. 대신, 결과를 내보내 도록 메소드를 전환 할 수 있습니다 . 가장 간단한 방법은 Action<IDataRecord>각 반복에서 호출 되는를 전달하는 것입니다.

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

항목 모음을 다루는 경우 IObservable/ / IObserver는 약간 더 적절한 데이터 구조 일 수 있지만 필요하지 않은 경우 단순성 Action이 훨씬 간단합니다.

열심히 평가하다

대안은 반환하기 전에 반복이 완전히 완료되도록하는 것입니다.

일반적으로 결과를 목록에 넣은 다음 반환하여이 작업을 수행 할 수 있지만,이 경우 각 항목에 독자와 동일한 참조가 추가로 복잡해집니다. 따라서 독자로부터 필요한 결과를 추출 할 무언가가 필요합니다.

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

Eagerly Evaluate라는 접근 방식을 사용하고 있습니다. 이제 SQLCommand의 Datareader가 myReader.NextResult ()와 같은 여러 출력을 반환하고 목록 목록을 구성하면 메모리 오버 헤드가됩니까? 아니면 데이터 리더를 발신자에게 보내는 것이 [개인적으로 선호하지 않습니다] 훨씬 효율적입니까? [하지만 연결이 더 오래 열립니다]. 내가 여기서 혼란스럽게하는 것은 목록 목록을 만드는 것보다 연결을 더 오래 열어 두는 것 사이의 균형입니다. DB에 연결된 내 웹 페이지를 방문하는 사람이 약 500 명 이상이라고 가정 할 수 있습니다.
GawdePrasad

1
@GawdePrasad 발신자가 독자와 함께 무엇을 할 것인지에 달려 있습니다. 컬렉션 자체에 항목을 넣는 경우 상당한 오버 헤드가 없습니다. 메모리에 많은 양의 값을 유지하지 않아도되는 작업을 수행하는 경우 메모리 오버 헤드가 있습니다. 그러나 매우 많은 양을 저장하지 않으면주의해야 할 오버 헤드가 아닐 수 있습니다. 어느 쪽이든, 상당한 시간 오버 헤드가 없어야합니다.
Ben Aaronson

"푸시, 당기지 않음"접근 방식은 "결과에 대한 반복이 완료 될 때까지 연결을 계속 열어 둘 것"문제를 어떻게 해결합니까? 이 방법으로도 반복을 완료 할 때까지 연결은 계속 열려 있습니다.
BornToCode

1
@BornToCode 질문 : "아래에 표시된 것처럼 yield return을 사용하고 전체 DataReader를 반복하지 않으면 DB 연결이 영구적으로 열려 있습니까?"라는 질문을 참조하십시오. 문제는 IEnumerable을 반환하고 처리하는 모든 발신자가이 특별한 문제를 알고 있어야한다는 것입니다. "나를 반복하지 않으면 연결을 유지합니다." 푸시 방법에서는 호출자로부터 해당 책임을지게됩니다. 부분적으로 반복 할 수있는 옵션이 없습니다. 시간 제어가 리턴되면 연결이 닫힙니다.
벤 애런 슨

1
이것은 훌륭한 답변입니다. 페이지 방식으로 표시되는 큰 쿼리가있는 경우 동작 <> 대신 Func <>을 전달하여 읽기 속도를 미리 중단 할 수 있기 때문에 푸시 방식이 마음에 듭니다. GetRecords 함수가 페이징을 수행하는 것보다 깨끗합니다.
Whelkaholism

10

귀하의 finally블록은 항상 실행됩니다.

yield return컴파일러 를 사용 하면 상태 머신을 구현하기 위해 새 중첩 클래스가 생성됩니다.

이 클래스는 finally블록의 모든 코드를 별도의 메소드로 포함합니다 . finally상태에 따라 실행해야하는 블록을 추적 합니다. 필요한 모든 finally블록이 Dispose방법 으로 실행됩니다 .

C # 언어 사양에 foreach (V v in x) embedded-statement따라

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

break또는로 루프를 종료하더라도 열거자가 삭제됩니다 return.

반복자 구현에 대한 자세한 정보를 얻으려면 Jon Skeet의이 기사를 읽으십시오.

편집하다

이러한 접근 방식의 문제점은 클래스의 클라이언트를 사용하여 메소드를 올바르게 사용한다는 것입니다. 예를 들어 열거자를 직접 가져 와서 while배치하지 않고 루프 에서 반복 할 수 있습니다.

@BenAaronson이 제안한 솔루션 중 하나를 고려해야합니다.


1
이것은 매우 신뢰할 수 없습니다. 예를 들어 var records = GetRecords(); var first = records.First(); var count = records.Count(), 실제로 매번 연결과 판독기를 열고 닫는 방식으로 해당 메소드를 두 번 실행합니다. 그것을 두 번 반복하면 같은 일을합니다. 실제로 결과를 반복하기 전에 오랜 시간 동안 결과를 전달할 수도 있습니다. 메소드 서명의 어떤 것도 그러한 행동을 의미하지 않습니다.
Ben Aaronson

2
@ BenAaronson 나는 내 대답에서 현재 구현에 대한 특정 질문에 집중했습니다. 전체 문제를 살펴보면 대답에서 제안한 방식을 사용하는 것이 훨씬 낫다는 데 동의합니다.
Jakub Lortz

1
@JakubLortz Fair 충분히
Ben Aaronson

2
@EsbenSkovPedersen 일반적으로 바보 같은 것을 만들려고하면 일부 기능이 손실됩니다. 균형을 잡아야합니다. 여기서 우리는 열심히 결과를로드하여 많은 것을 잃지 않습니다. 어쨌든, 가장 큰 문제는 명명입니다. GetRecordsreturning 이라는 메소드가 표시되면 IEnumerable레코드를 메모리에로드 할 것으로 예상됩니다. 반면에 그것이 property Records라면 데이터베이스에 대한 추상화라고 가정합니다.
Jakub Lortz

1
@EsbenSkovPedersen 음, nvoigt의 답변에 대한 내 의견을 참조하십시오. 나는 방법을 통해 반복 할 때 중요한 일을 할 것 IEnumerable을 반환에 일반적으로 아무런 문제가 없지만,이 경우에는 메소드 서명의 의미를 적합하지 않는 것
벤 아 론슨

5

특정 yield동작에 관계없이 코드에는 리소스를 올바르게 폐기하지 않는 실행 경로가 포함되어 있습니다. 두 번째 줄에서 예외가 발생하거나 세 번째 줄에서 예외가 발생하면 어떻게합니까? 아니면 네번째? 매우 복잡한 try / finally chain이 필요하거나 using블록 을 사용할 수 있습니다 .

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

사람들은 이것이 직관적이지 않다고 말했으며, 메소드를 호출하는 사람들은 결과를 두 번 열거하면 메소드를 두 번 호출한다는 것을 모를 수 있습니다. 음, 힘든 행운. 그것이 언어가 작동하는 방식입니다. 그것이 IEnumerable<T>보내는 신호입니다 . 이 반환하지 않는 이유가있다 List<T>거나 T[]. 이 사실을 모르는 사람들은 교육을 받아야하며 해결해야합니다.

Visual Studio에는 정적 코드 분석이라는 기능이 있습니다. 이를 사용하여 자원을 올바르게 배치했는지 확인할 수 있습니다.


이 언어를 사용하면 IEnumerable완전히 관련이없는 예외를 던지거나 5GB 파일을 디스크에 쓰는 것과 같은 모든 종류의 작업을 반복 할 수 있습니다 . 언어가 허용한다고해서 그것이 IEnumerable일어날 것이라는 표시를 제공하지 않는 메소드에서 반환하는 것이 허용되는 것은 아닙니다 .
Ben Aaronson

1
반환하는 IEnumerable<T> 것입니다 두 번 열거하는 두 통화 초래할 것이라고 표시. 열거 가능한 여러 번 열거하면 도구에 유용한 경고가 표시됩니다. 예외 발생에 대한 요점은 반환 유형에 관계없이 동일하게 유효합니다. 해당 메소드가를 반환 List<T>하면 동일한 예외가 발생합니다.
nvoigt

나는 당신이 당신의 요점을 이해하고 어느 정도 당신이 IEnumerable그때 당신에게 주의를 기울이는 방법을 소비한다면 동의합니다 . 그러나 나는 또한 메소드 작성자로서 서명이 의미하는 바를 준수 할 책임이 있다고 생각합니다. 이 메소드는 호출 GetRecords되므로 리턴하면 레코드를 얻었 을 것으로 예상됩니다 . 그것이 GiveMeSomethingICanPullRecordsFrom(또는 더 현실적으로 GetRecordSource또는 유사하게) 불린다면 게으른 IEnumerable것이 더 수용 가능할 것입니다.
Ben Aaronson

나는, 예를 들어 그 동의 @BenAaronson File, ReadLines그리고 ReadAllLines쉽게 떨어져 말했다 수 있습니다 그것은 좋은 것입니다. (하지만 메서드 오버로드는 반환 유형에 따라 다를 수 없기 때문에 더 큽니다). 아마도 ReadRecords와 ReadAllRecords도 여기에 이름 지정 체계로 적합 할 것입니다.
nvoigt

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.