문제

배경:데이터베이스에서 가져온 문자열이 여러 개 있어서 이를 반환하고 싶습니다.전통적으로 다음과 같습니다.

public List<string> GetStuff(string connectionString)
{
    List<string> categoryList = new List<string>();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                categoryList.Add(sqlDataReader["myImportantColumn"].ToString());
            }
        }
    }
    return categoryList;
}

하지만 소비자가 항목을 반복하기를 원할 것이고 다른 것은 별로 신경 쓰지 않을 것이라고 생각하며 본질적으로 목록에 자신을 넣지 않기를 원하므로 IEnumerable을 반환하면 모든 것이 좋습니다. /유연한.그래서 저는 이것을 처리하기 위해 "수익률" 유형의 디자인을 사용할 수 있다고 생각했습니다...다음과 같습니다:

public IEnumerable<string> GetStuff(string connectionString)
{
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                yield return sqlDataReader["myImportantColumn"].ToString();
            }
        }
    }
}

하지만 이제 수익률에 대해 좀 더 읽고 있으니(이런 사이트에서...msdn은 이 내용을 언급하지 않은 것 같습니다) 누군가가 요구할 것을 예상하여 채우기 상태를 유지하는 게으른 평가자인 것 같습니다. 다음 값에 대해서는 다음 값을 반환할 때까지만 실행합니다.

대부분의 경우에는 괜찮아 보이지만 DB 호출의 경우 약간 이상하게 들립니다.다소 인위적인 예로서, 누군가가 DB 호출에서 채우는 IEnumerable을 요청하면 그 중 절반을 통과한 다음 루프에 갇히게 됩니다... 제가 볼 수 있는 한 DB 연결이 진행 중입니다. 영원히 열려 있도록.

반복자가 완료되지 않으면 어떤 경우에는 문제가 발생하는 것처럼 들리는데... 뭔가 빠졌나요?

도움이 되었습니까?

해결책

균형 행위입니다. 모든 데이터를 즉시 메모리로 강제하여 연결을 확보 할 수 있거나 모든 시간 동안 연결을 연결하는 비용으로 데이터를 스트리밍하여 혜택을 받고 싶습니까?

내가 보는 방식, 그 결정은 잠재적으로 자신이하고 싶은 일에 대해 더 많이 알고있는 발신자에게 달려야합니다. 반복자 블록을 사용하여 코드를 작성하면 발신자가 매우 그 스트리밍 양식을 완전히 완전한 형태로 쉽게 돌 렸습니다.

List<string> stuff = new List<string>(GetStuff(connectionString));

반면에, 당신이 버퍼링을 직접 수행한다면, 발신자가 스트리밍 모델로 돌아갈 수있는 방법은 없습니다.

그래서 나는 아마 스트리밍 모델을 사용하고 말할 것입니다 명시 적으로 문서에서 그것이 무엇을하는지, 발신자에게 적절하게 결정하도록 조언합니다. 기본적으로 스트리밍 된 버전을 호출하여 목록으로 변환하는 도우미 방법을 제공 할 수도 있습니다.

물론, 발신자가 적절한 결정을 내릴 것을 믿지 않고, 그들이 실제로 데이터를 스트리밍하고 싶지 않을 것이라고 믿을만한 충분한 이유가 있다면 (예 : 어쨌든 많이 반환하지 않을 것입니다) 목록을 찾으십시오. 접근하다. 어느 쪽이든, 문서화 - 반환 값이 사용되는 방식에 큰 영향을 줄 수 있습니다.

많은 양의 데이터를 다루는 또 다른 옵션은 물론 배치를 사용하는 것입니다. 물론 원래 질문에서 다소 멀어지고 있지만 스트리밍이 일반적으로 매력적인 상황에서 고려해야 할 다른 접근법입니다.

다른 팁

당신은 항상 ienumerable에 대해 안전하지는 않습니다. 프레임 워크 호출을 떠나는 경우 GetEnumerator (대부분의 사람들이 할 일), 당신은 안전합니다. 기본적으로 방법을 사용하여 코드의 신중함만큼 안전합니다.

class Program
{
    static void Main(string[] args)
    {
        // safe
        var firstOnly = GetList().First();

        // safe
        foreach (var item in GetList())
        {
            if(item == "2")
                break;
        }

        // safe
        using (var enumerator = GetList().GetEnumerator())
        {
            for (int i = 0; i < 2; i++)
            {
                enumerator.MoveNext();
            }
        }

        // unsafe
        var enumerator2 = GetList().GetEnumerator();

        for (int i = 0; i < 2; i++)
        {
            enumerator2.MoveNext();
        }
    }

    static IEnumerable<string> GetList()
    {
        using (new Test())
        {
            yield return "1";
            yield return "2";
            yield return "3";
        }
    }

}

class Test : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("dispose called");
    }
}

데이터베이스 연결을 열어 놓을 수 있는지 여부는 아키텍처에 따라 다릅니다. 발신자가 트랜잭션에 참여하고 연결이 자동 입대하면 연결이 프레임 워크에 의해 열려 있습니다.

또 다른 장점 yield (서버 측 커서를 사용할 때)는 소비자가 루프에서 일찍 나가려고하는 경우 (예 : 10 번째 항목 이후) 코드가 데이터베이스에서 모든 데이터 (예 : 1,000 항목)를 읽을 필요는 없습니다. . 쿼리 데이터 속도를 높일 수 있습니다. 특히 서버 측 커서가 데이터를 검색하는 일반적인 방법 인 Oracle 환경에서.

당신은 아무것도 놓치지 않았습니다.귀하의 샘플은 수익률 반환을 사용하지 않는 방법을 보여줍니다.목록에 항목을 추가하고 연결을 닫은 후 목록을 반환합니다.메서드 서명은 여전히 ​​IEnumerable을 반환할 수 있습니다.

편집하다: 즉, Jon의 주장에는 일리가 있습니다(너무 놀랐습니다!).성능 측면에서 스트리밍이 실제로 가장 좋은 경우는 거의 없습니다.결국 10만(100만?10,000,000?) 행에 대해 여기서 이야기하고 있지만 먼저 모든 행을 메모리에 로드하고 싶지는 않을 것입니다.

따로 - IEnumerable<T> 접근 방식입니다 본질적으로 LINQ 제공 업체 (LINQ-to-SQL, LINQ-to-ENTISE)가 생계를 위해하는 일. Jon이 말한 것처럼 접근 방식은 장점이 있습니다. 그러나 분명한 문제도 있습니다. 특히 (나에게) 분리의 조합 측면에서 | 추출.

여기서 의미하는 바는 다음과 같습니다.

  • MVC 시나리오에서 (예를 들어) "데이터 가져 오기"단계를 원합니다. 실제로 데이터를 얻습니다, 당신이 그것을 테스트 할 수 있도록 제어 장치, 보다 (전화를 기억할 필요없이 .ToList() 등)
  • 다른 DAL 구현이 될 것이라고 보장 할 수는 없습니다 할 수 있는 데이터를 스트리밍하려면 (예 : POX/WSE/SOAP Call은 일반적으로 레코드를 스트리밍 할 수 없습니다); 그리고 당신은 반드시 행동을 혼란스럽게 다르게 만들고 싶지는 않습니다 (즉, 하나의 구현과 반복하는 동안 여전히 연결이 열리고 다른 구현을 위해 닫힙니다).

이것은 여기에 내 생각과 조금 관련이 있습니다. 실용적인 linq.

그러나 나는 스트레스가 매우 바람직한시기가 분명히 있습니다. 단순한 "항상 대비 절대"일이 아닙니다 ...

반복자 평가를 강요하는 약간 더 간결한 방법 :

using System.Linq;

//...

var stuff = GetStuff(connectionString).ToList();

아니요, 당신은 올바른 경로에 있습니다 ... 수익률은 독자를 잠그게합니다 ... ienumerable을 호출하는 동안 다른 데이터베이스 호출을 테스트 할 수 있습니다.

이것이 문제를 일으킬 수있는 유일한 방법은 발신자가 프로토콜을 남용하는 것입니다. IEnumerable<T>. 그것을 사용하는 올바른 방법은 전화하는 것입니다 Dispose 더 이상 필요하지 않을 때.

생성 된 구현 yield return 테이크 Dispose 열기를 실행하기 위해 신호로 호출하십시오 finally 예제에서 호출되는 블록 Dispose 당신이 만든 객체에서 using 진술.

여러 언어 기능이 있습니다 (특히 foreach) 사용하기가 매우 쉽습니다 IEnumerable<T> 바르게.

항상 별도의 스레드를 사용하여 데이터를 버퍼링하면서 데이터를 반환하기 위해 예수를 수행 할 수 있습니다. 사용자가 데이터를 요청하면 (예 실드를 통해 반환 됨) 항목이 큐에서 제거됩니다. 별도의 스레드를 통해 데이터도 큐에 지속적으로 추가되고 있습니다. 이렇게하면 사용자가 데이터를 충분히 빨리 요청하면 큐가 가득 차지 않으며 메모리 문제에 대해 걱정할 필요가 없습니다. 그렇지 않으면 대기열이 채워지면 그렇게 나쁘지 않을 수 있습니다. 메모리에 부과하려는 일종의 한계가 있으면 최대 큐 크기를 시행 할 수 있습니다 (이 시점에서 다른 스레드는 큐에 더 많이 추가하기 전에 항목을 제거 할 때까지 기다립니다). 당연히 두 스레드 사이에서 자원 (예 : 대기열)을 올바르게 처리해야합니다.

대안으로, 사용자가 부울으로 전달하여 데이터를 버퍼링 해야하는지 여부를 표시하도록 강요 할 수 있습니다. 사실이라면 데이터가 버퍼링되고 연결이 가능한 빨리 닫힙니다. False 인 경우 데이터가 버퍼링되지 않고 사용자가 필요한 한 데이터베이스 연결이 열려 있습니다. 부울 매개 변수가 있으면 사용자가 선택을 강요하여 문제에 대해 알 수 있습니다.

나는이 벽에 몇 번 부딪쳤다. SQL 데이터베이스 쿼리는 파일처럼 쉽게 스트리밍 할 수 없습니다. 대신, 필요하다고 생각하는만큼만 쿼리하고 원하는 컨테이너로 반환합니다 (IList<>, DataTable, 등.). IEnumerable 여기서 당신을 도와주지 않을 것입니다.

당신이 할 수있는 일은 대신 sqldataadapter를 사용하고 DataTable을 채우는 것입니다. 이 같은:

public IEnumerable<string> GetStuff(string connectionString)
{
    DataTable table = new DataTable();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;
            SqlDataAdapter dataAdapter = new SqlDataAdapter(sqlCommand);
            dataAdapter.Fill(table);
        }

    }
    foreach(DataRow row in table.Rows)
    {
        yield return row["myImportantColumn"].ToString();
    }
}

이런 식으로, 한 번의 모든 것을 쿼리하고 즉시 연결을 닫지 만 여전히 결과를 게으르게 반복하고 있습니다. 또한,이 방법의 발신자는 결과를 목록에 캐스팅하고하지 말아야 할 일을 할 수 없습니다.

여기서 수율을 사용하지 마십시오. 샘플은 괜찮습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top