Kết nối cơ sở dữ liệu sẽ bị đóng nếu chúng ta mang lại hàng dữ liệu và không đọc tất cả các bản ghi?


14

Trong khi hiểu cách yieldhoạt động của từ khóa, tôi đã xem qua link1link2 trên StackOverflow, chủ trương sử dụng yield returntrong khi lặp qua DataReader và nó cũng phù hợp với nhu cầu của tôi. Nhưng nó làm tôi tự hỏi điều gì sẽ xảy ra, nếu tôi sử dụng yield returnnhư được hiển thị bên dưới và nếu tôi không lặp qua toàn bộ DataReader, liệu kết nối DB có mở mãi mãi không?

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

Tôi đã thử ví dụ tương tự trong Ứng dụng Console mẫu và nhận thấy trong khi gỡ lỗi rằng khối cuối cùng GetRecords()không được thực thi. Làm thế nào tôi có thể đảm bảo sau đó đóng kết nối DB? Có cách nào tốt hơn là sử dụng yieldtừ khóa không? Tôi đang cố gắng thiết kế một lớp tùy chỉnh sẽ chịu trách nhiệm thực thi các SQL được chọn và các thủ tục được lưu trữ trên DB và sẽ trả về kết quả. Nhưng tôi không muốn trả lại DataReader cho người gọi. Ngoài ra tôi muốn đảm bảo rằng kết nối sẽ được đóng lại trong tất cả các tình huống.

Biên tập Thay đổi câu trả lời cho câu trả lời của Ben vì không chính xác khi mong đợi người gọi phương thức sử dụng phương thức này một cách chính xác và đối với kết nối DB sẽ tốn kém hơn nếu phương thức được gọi nhiều lần mà không có lý do.

Cảm ơn Jakob và Ben đã giải thích chi tiết.


Từ những gì tôi thấy bạn không mở kết nối ở nơi đầu tiên
paparazzo

@Frisbee Đã chỉnh sửa :)
GawdePrasad

Câu trả lời:


12

Có, bạn sẽ phải đối mặt với vấn đề bạn mô tả: cho đến khi bạn hoàn thành việc lặp lại kết quả, bạn sẽ giữ kết nối mở. Có hai cách tiếp cận chung mà tôi có thể nghĩ ra để giải quyết vấn đề này:

Đẩy, đừng kéo

Hiện tại bạn đang trả về một IEnumerable<IDataRecord>cấu trúc dữ liệu bạn có thể lấy từ đó. Thay vào đó, bạn có thể chuyển đổi phương pháp của mình để đẩy kết quả của nó ra. Cách đơn giản nhất là vượt qua trong một lệnh Action<IDataRecord>được gọi trên mỗi lần lặp:

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

Lưu ý rằng bạn đang xử lý một bộ sưu tập các mặt hàng, IObservable/ IObservercó thể là một cấu trúc dữ liệu phù hợp hơn một chút, nhưng trừ khi bạn cần nó, một cách đơn giản Actionsẽ đơn giản hơn nhiều.

Đánh giá háo hức

Một cách khác là chỉ để đảm bảo việc lặp lại hoàn thành trước khi quay trở lại.

Bạn thường có thể làm điều này bằng cách chỉ đưa các kết quả vào một danh sách sau đó trả lại, nhưng trong trường hợp này có sự phức tạp thêm của mỗi mục là cùng một tham chiếu đến người đọc. Vì vậy, bạn cần một cái gì đó để trích xuất kết quả bạn cần từ người đọc:

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

Tôi đang sử dụng cách tiếp cận mà bạn đặt tên là Háo hức Đánh giá. Bây giờ nó sẽ là chi phí bộ nhớ nếu Datareader của SQLCommand của tôi trả về nhiều kết quả đầu ra (như myReader.NextResult ()) và tôi xây dựng một danh sách danh sách? Hoặc gửi dữ liệu cho người gọi [Cá nhân tôi không thích nó] sẽ hiệu quả hơn nhiều? [nhưng kết nối sẽ được mở lâu hơn]. Điều tôi chính xác bối rối ở đây là giữa sự đánh đổi giữ cho kết nối mở lâu hơn là tạo một danh sách danh sách. Bạn có thể giả định rằng sẽ có gần 500 người truy cập trang web của tôi kết nối với DB.
GawdePrasad

1
@GawdePrasad Nó phụ thuộc vào những gì người gọi sẽ làm với người đọc. Nếu nó chỉ đặt các mục trong một bộ sưu tập, thì không có chi phí đáng kể. Nếu nó thực hiện một thao tác không yêu cầu giữ toàn bộ nhiều giá trị trong bộ nhớ, thì sẽ có chi phí bộ nhớ. Tuy nhiên, trừ khi bạn đang lưu trữ số lượng rất lớn, có lẽ đó không phải là chi phí bạn nên quan tâm. Dù bằng cách nào, không nên có một khoảng thời gian đáng kể.
Ben Aaronson

Làm thế nào để phương pháp "đẩy, không kéo" giải quyết vấn đề "cho đến khi bạn hoàn thành việc lặp lại kết quả, bạn sẽ giữ kết nối mở"? Kết nối vẫn được giữ cho đến khi bạn hoàn thành việc lặp lại ngay cả với phương pháp này, phải không?
Sinh ra ToCode

1
@BornToCode Xem câu hỏi: "nếu tôi sử dụng lợi nhuận như được hiển thị bên dưới và nếu tôi không lặp qua toàn bộ DataReader, kết nối DB sẽ mở mãi mãi". Vấn đề là bạn trả lại một IEnumerable và mọi người gọi xử lý nó phải nhận thức được điều đặc biệt này: "Nếu bạn không hoàn thành việc lặp lại qua tôi, tôi sẽ giữ kết nối mở". Trong phương pháp đẩy, bạn đưa trách nhiệm đó ra khỏi người gọi - họ không có tùy chọn lặp lại một phần. Bởi kiểm soát thời gian được trả lại cho họ, kết nối được đóng lại.
Ben Aaronson

1
Đây là một câu trả lời tuyệt vời. Tôi thích cách tiếp cận đẩy, bởi vì nếu bạn có một truy vấn lớn mà bạn đang trình bày theo kiểu phân trang, bạn có thể hủy bỏ việc đọc trước đó để tăng tốc độ bằng cách chuyển qua Func <> thay vì Hành động <>; điều này cảm thấy sạch hơn so với làm cho chức năng GetRecords cũng thực hiện phân trang.
Chủ nghĩa cá nhân

10

finallyKhối của bạn sẽ luôn luôn thực thi.

Khi bạn sử dụng yield returntrình biên dịch sẽ tạo một lớp lồng nhau mới để thực hiện một máy trạng thái.

Lớp này sẽ chứa tất cả các mã từ finallycác khối như các phương thức riêng biệt. Nó sẽ theo dõi các finallykhối cần được thực hiện tùy thuộc vào trạng thái. Tất cả finallycác khối cần thiết sẽ được thực hiện trong Disposephương thức.

Theo đặc tả ngôn ngữ C #, foreach (V v in x) embedded-statementđược mở rộng thành

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

vì vậy, điều tra viên sẽ được xử lý ngay cả khi bạn thoát khỏi vòng lặp với breakhoặc return.

Để có thêm thông tin về việc thực hiện iterator, bạn có thể đọc bài viết này của Jon Skeet

Biên tập

Vấn đề với cách tiếp cận như vậy là bạn dựa vào các khách hàng của lớp để sử dụng phương thức này một cách chính xác. Ví dụ, họ có thể lấy điều tra viên trực tiếp và lặp qua nó trong một whilevòng lặp mà không cần xử lý nó.

Bạn nên xem xét một trong những giải pháp được đề xuất bởi @BenAaronson.


1
Điều này là vô cùng không đáng tin cậy. Ví dụ: var records = GetRecords(); var first = records.First(); var count = records.Count()sẽ thực sự thực hiện phương thức đó hai lần, mở và đóng kết nối và đầu đọc mỗi lần. Lặp lại qua nó hai lần làm điều tương tự. Bạn cũng có thể vượt qua kết quả trong một thời gian dài trước khi thực sự lặp lại nó, v.v. Không có gì trong chữ ký phương thức ngụ ý loại hành vi đó
Ben Aaronson

2
@BenAaronson Tôi tập trung vào câu hỏi cụ thể về việc triển khai hiện tại trong câu trả lời của tôi. Nếu bạn nhìn vào toàn bộ vấn đề, tôi đồng ý sẽ tốt hơn nhiều khi sử dụng cách bạn đề xuất trong câu trả lời của mình.
Jakub Lortz

1
@JakubLortz Đủ công bằng
Ben Aaronson

2
@EsbenSkovPedersen Thông thường khi bạn cố gắng làm một cái gì đó ngớ ngẩn, bạn sẽ mất một số chức năng. Bạn cần phải cân bằng nó ra. Ở đây chúng tôi không mất nhiều bằng cách háo hức tải kết quả. Dù sao, vấn đề lớn nhất đối với tôi là việc đặt tên. Khi tôi thấy một phương thức có tên là GetRecordsreturn IEnumerable, tôi hy vọng nó sẽ tải các bản ghi vào bộ nhớ. Mặt khác, nếu đó là một tài sản Records, tôi thà cho rằng đó là một loại trừu tượng trên cơ sở dữ liệu.
Jakub Lortz

1
@EsbenSkovPedersen Vâng, hãy xem ý kiến ​​của tôi về câu trả lời của nvoigt. Nói chung, tôi không có vấn đề gì với các phương thức trả về IEnumerable sẽ thực hiện công việc quan trọng khi lặp đi lặp lại, nhưng trong trường hợp này dường như không phù hợp với ý nghĩa của chữ ký phương thức
Ben Aaronson

5

Bất kể yieldhành vi cụ thể , mã của bạn chứa các đường dẫn thực thi sẽ không xử lý tài nguyên của bạn một cách chính xác. Điều gì nếu dòng thứ hai của bạn ném một ngoại lệ hoặc thứ ba của bạn? Hay thậm chí là thứ tư của bạn? Bạn sẽ cần một chuỗi thử / cuối cùng rất phức tạp hoặc bạn có thể sử dụng usingcác khối.

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

Mọi người đã nhận xét rằng điều này không trực quan, rằng mọi người gọi phương thức của bạn có thể không biết rằng việc liệt kê kết quả hai lần sẽ gọi phương thức đó hai lần. Chà, may mắn khó khăn. Đó là cách ngôn ngữ hoạt động. Đó là tín hiệu IEnumerable<T>gửi đi. Có một lý do này không trở lại List<T>hoặc T[]. Những người không biết điều này cần phải được giáo dục, không được làm việc xung quanh.

Visual Studio có một tính năng gọi là phân tích mã tĩnh. Bạn có thể sử dụng nó để tìm hiểu xem bạn đã xử lý tài nguyên đúng cách chưa.


Ngôn ngữ cho phép lặp đi lặp lại IEnumerableđể làm tất cả mọi thứ, chẳng hạn như ném các ngoại lệ hoàn toàn không liên quan hoặc ghi các tệp 5GB vào đĩa. Chỉ vì ngôn ngữ cho phép nó không có nghĩa là nó có thể chấp nhận trả lại một ngôn ngữ IEnumerabletừ phương thức không có dấu hiệu cho thấy điều đó sẽ xảy ra.
Ben Aaronson

1
Trả lại một IEnumerable<T> dấu hiệu cho thấy việc liệt kê hai lần sẽ dẫn đến hai cuộc gọi. Bạn thậm chí sẽ nhận được các cảnh báo hữu ích trong các công cụ của mình nếu bạn liệt kê vô số lần. Điểm về ném ngoại lệ có giá trị như nhau bất kể loại trả về. Nếu phương thức đó trả về, List<T>nó sẽ đưa ra các ngoại lệ tương tự.
nvoigt

Tôi hiểu quan điểm của bạn và ở một mức độ nào đó tôi đồng ý - nếu bạn đang sử dụng một phương pháp mang lại cho bạn một công cụ làm trống IEnumerablesau đó. Nhưng tôi cũng nghĩ rằng là một người viết phương pháp, bạn có trách nhiệm tuân thủ những gì chữ ký của bạn ngụ ý. Phương thức này được gọi GetRecords, vì vậy khi nó trả về, bạn mong muốn nó có, bạn biết, đã nhận được các bản ghi . Nếu nó được gọi GiveMeSomethingICanPullRecordsFrom(hoặc, thực tế hơn GetRecordSourcehoặc tương tự) thì một kẻ lười biếng IEnumerablesẽ dễ chấp nhận hơn.
Ben Aaronson

@BenAaronson Tôi đồng ý rằng ví dụ như trong File, ReadLinesReadAllLinescó thể dễ dàng phân biệt và đó là một điều tốt. (Mặc dù nhiều hơn vì thực tế là quá tải phương thức không thể chỉ khác nhau bởi kiểu trả về). Có lẽ ReadRecords và ReadAllRecords cũng phù hợp ở đây là sơ đồ đặt tên.
nvoigt

2

Những gì bạn đã làm có vẻ sai, nhưng sẽ làm việc tốt.

Bạn nên sử dụng IEnumerator <> , vì nó cũng thừa hưởng IDis Dùng một lần .

Nhưng bởi vì bạn đang sử dụng một foreach, trình biên dịch vẫn đang sử dụng IDis Dùng để tạo một IEnumerator <> cho foreach.

trong thực tế, các bài giảng liên quan đến rất nhiều điều.

Kiểm tra /programming/13459447/do-i-need-to-consider-disposeing-of-any-ienumerablet-i-use

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.