Apakah koneksi Database akan ditutup jika kami menghasilkan baris datareader dan tidak membaca semua catatan?


15

Sementara memahami cara yieldkerja kata kunci, saya menemukan link1 dan link2 di StackOverflow yang menganjurkan penggunaan yield returnsementara iterasi DataReader dan sesuai dengan kebutuhan saya juga. Tapi itu membuat saya bertanya-tanya seperti apa yang terjadi, jika saya menggunakan yield returnseperti yang ditunjukkan di bawah ini dan jika saya tidak mengulangi seluruh DataReader, akankah koneksi DB tetap terbuka selamanya?

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

Saya mencoba contoh yang sama dalam contoh Aplikasi Konsol dan memperhatikan saat debugging bahwa blok akhirnya GetRecords()tidak dijalankan. Bagaimana saya memastikan penutupan Koneksi DB? Apakah ada cara yang lebih baik daripada menggunakan yieldkata kunci? Saya mencoba merancang kelas khusus yang akan bertanggung jawab untuk mengeksekusi SQL pilihan dan prosedur tersimpan pada DB dan akan mengembalikan hasilnya. Tapi saya tidak ingin mengembalikan DataReader ke pemanggil. Saya juga ingin memastikan bahwa koneksi akan ditutup di semua skenario.

Sunting Mengubah jawaban atas jawaban Ben karena tidak tepat mengharapkan penelepon metode menggunakan metode ini dengan benar dan sehubungan dengan koneksi DB akan lebih mahal jika metode ini dipanggil beberapa kali tanpa alasan.

Terima kasih Jakob dan Ben untuk penjelasan terperinci.


Dari apa yang saya lihat, Anda tidak membuka koneksi di tempat pertama
paparazzo

@Frisbee Diedit :)
GawdePrasad

Jawaban:


12

Ya, Anda akan menghadapi masalah yang Anda uraikan: sampai Anda selesai mengulangi hasilnya, Anda akan membuat koneksi tetap terbuka. Ada dua pendekatan umum yang bisa saya pikirkan untuk menghadapi ini:

Dorong, jangan tarik

Saat Anda kembali sebuah IEnumerable<IDataRecord>, struktur data Anda dapat menarik dari. Sebagai gantinya, Anda dapat mengganti metode Anda untuk mendorong hasilnya. Cara paling sederhana adalah dengan memasukkan sebuah Action<IDataRecord>yang dipanggil pada setiap iterasi:

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

Perhatikan bahwa mengingat Anda berurusan dengan koleksi item, IObservable/ IObservermungkin struktur data sedikit lebih tepat, tetapi kecuali jika Anda membutuhkannya, yang sederhana Actionjauh lebih mudah.

Mengevaluasi dengan bersemangat

Alternatif lain adalah memastikan iterasi sepenuhnya selesai sebelum kembali.

Anda biasanya dapat melakukan ini dengan hanya memasukkan hasilnya dalam daftar lalu mengembalikannya, tetapi dalam hal ini ada komplikasi tambahan dari setiap item yang menjadi referensi yang sama kepada pembaca. Jadi Anda perlu sesuatu untuk mengekstrak hasil yang Anda butuhkan dari pembaca:

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

Saya menggunakan pendekatan yang Anda beri nama Eagerly Evaluate. Sekarang apakah ini akan menjadi overhead memori jika Datareader SQLCommand saya mengembalikan banyak output (seperti myReader.NextResult ()) dan saya membuat daftar daftar? Atau mengirim datareader ke pemanggil [saya pribadi tidak suka] akan jauh lebih efisien? [tetapi koneksi akan terbuka lebih lama]. Yang saya justru bingung di sini adalah antara tradeoff menjaga koneksi terbuka lebih lama daripada membuat daftar daftar. Anda dapat dengan aman berasumsi bahwa akan ada hampir 500+ orang yang mengunjungi halaman web saya yang terhubung ke DB.
GawdePrasad

1
@ GawdePrasad Tergantung apa yang akan dilakukan penelepon dengan pembaca. Jika itu hanya akan menempatkan item dalam koleksi itu sendiri, tidak ada overhead yang signifikan. Jika itu akan melakukan operasi yang tidak memerlukan menjaga seluruh nilai dalam memori, maka ada overhead memori. Namun, kecuali jika Anda menyimpan jumlah yang sangat besar, itu mungkin bukan overhead yang harus Anda pedulikan. Either way, seharusnya tidak ada overhead waktu yang signifikan.
Ben Aaronson

Bagaimana pendekatan "push, don't pull" menyelesaikan masalah "sampai Anda selesai mengulangi hasilnya, Anda akan membuat koneksi tetap terbuka"? Koneksi masih tetap terbuka sampai Anda menyelesaikan iterasi bahkan dengan pendekatan ini, bukan?
BornToCode

1
@BornToCode Lihat pertanyaan: "jika saya menggunakan pengembalian hasil seperti yang ditunjukkan di bawah ini dan jika saya tidak mengulangi seluruh DataReader, akankah koneksi DB tetap terbuka selamanya". Masalahnya adalah Anda mengembalikan IEnumerable dan setiap penelepon yang menanganinya harus mengetahui gotcha khusus ini: "Jika Anda tidak selesai mengulanginya atas saya, saya akan membuka koneksi". Dalam metode push, Anda mengambil tanggung jawab itu dari penelepon - mereka tidak memiliki opsi untuk beralih sebagian. Pada saat kontrol dikembalikan kepada mereka, koneksi ditutup.
Ben Aaronson

1
Ini jawaban yang sangat bagus. Saya suka pendekatan push, karena jika Anda memiliki kueri besar yang Anda presentasikan dalam mode halaman, Anda dapat membatalkan bacaan lebih awal untuk kecepatan dengan melewati Func <> daripada Action <>; ini terasa lebih bersih daripada membuat fungsi GetRecords juga melakukan paging.
Whelkaholism

10

Anda finallyBlok akan selalu dieksekusi.

Saat Anda menggunakan yield return kompiler akan membuat kelas bersarang baru untuk mengimplementasikan mesin negara.

Kelas ini akan berisi semua kode dari finallyblok sebagai metode terpisah. Ini akan melacak finallyblok yang perlu dijalankan tergantung pada negara. Semua finallyblok yang diperlukan akan dieksekusi diDispose metode.

Menurut spesifikasi bahasa C #, foreach (V v in x) embedded-statementdiperluas ke

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

sehingga enumerator akan dibuang meskipun Anda keluar dari loop dengan breakatau return.

Untuk mendapatkan informasi lebih lanjut tentang implementasi iterator Anda dapat membaca artikel ini oleh Jon Skeet

Edit

Masalah dengan pendekatan tersebut adalah bahwa Anda mengandalkan klien kelas Anda untuk menggunakan metode ini dengan benar. Misalnya mereka bisa mendapatkan enumerator secara langsung dan mengulanginya melalui awhile lingkaran tanpa membuangnya.

Anda harus mempertimbangkan salah satu solusi yang disarankan oleh @BenAaronson.


1
Ini sangat tidak bisa diandalkan. Sebagai contoh: var records = GetRecords(); var first = records.First(); var count = records.Count()sebenarnya akan menjalankan metode itu dua kali, membuka dan menutup koneksi dan pembaca setiap kali. Iterasi dua kali melakukan hal yang sama. Anda juga dapat melewati hasilnya untuk waktu yang lama sebelum benar-benar mengulanginya, dll. Tidak ada dalam tanda tangan metode menyiratkan perilaku semacam itu
Ben Aaronson

2
@ BenAaronson Saya fokus pada pertanyaan spesifik tentang implementasi saat ini dalam jawaban saya. Jika Anda melihat keseluruhan masalahnya, saya setuju bahwa jauh lebih baik menggunakan cara yang Anda sarankan dalam jawaban Anda.
Jakub Lortz

1
@ JakubLortz Cukup adil
Ben Aaronson

2
@EsbenSkovPedersen Biasanya ketika Anda mencoba membuat sesuatu yang bodoh, Anda kehilangan beberapa fungsi. Anda harus menyeimbangkannya. Di sini kita tidak kehilangan banyak dengan dengan bersemangat memuat hasilnya. Bagaimanapun, masalah terbesar bagi saya adalah penamaan. Ketika saya melihat metode bernama GetRecordsreturn IEnumerable, saya berharap itu memuat catatan ke memori. Di sisi lain, jika itu adalah properti Records, saya lebih suka menganggap itu semacam abstraksi atas database.
Jakub Lortz

1
@EsbenSkovPedersen Baiklah, lihat komentar saya pada jawaban nvoigt. Saya tidak memiliki masalah secara umum dengan metode mengembalikan IEnumerable yang akan melakukan pekerjaan yang signifikan ketika diulangi, tetapi dalam kasus ini tampaknya tidak sesuai dengan implikasi dari tanda tangan metode
Ben Aaronson

5

Terlepas dari yieldperilaku tertentu , kode Anda berisi jalur eksekusi yang tidak akan membuang sumber daya Anda dengan benar. Bagaimana jika baris kedua Anda melempar pengecualian atau yang ketiga? Atau bahkan keempat Anda? Anda membutuhkan rantai coba / akhirnya yang sangat kompleks, atau Anda dapat menggunakan usingblok.

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

Orang-orang mengatakan bahwa ini tidak intuitif, bahwa orang yang memanggil metode Anda mungkin tidak tahu bahwa menghitung hasilnya dua kali akan memanggil metode dua kali. Yah, keberuntungan yang sulit. Begitulah cara kerjanya bahasa. Itu sinyal yang IEnumerable<T>mengirim. Ada alasan mengapa ini tidak kembali List<T>atau T[]. Orang yang tidak tahu ini perlu dididik, tidak bekerja.

Visual Studio memiliki fitur yang disebut analisis kode statis. Anda dapat menggunakannya untuk mengetahui apakah Anda telah membuang sumber daya dengan benar.


Bahasa ini memungkinkan iterasi IEnumerableuntuk melakukan segala macam hal, seperti melempar pengecualian yang sama sekali tidak terkait atau menulis file 5GB ke disk. Hanya karena bahasa memungkinkan, bukan berarti dapat diterima untuk mengembalikan IEnumerableyang melakukannya dari metode yang tidak memberikan indikasi bahwa itu akan terjadi.
Ben Aaronson

1
Mengembalikan IEnumerable<T> adalah indikasi bahwa penghitungan dua kali akan menghasilkan dua panggilan. Anda bahkan akan mendapatkan peringatan yang berguna di alat Anda jika Anda menghitung berulang kali. Poin tentang melempar pengecualian sama-sama valid terlepas dari tipe pengembalian. Jika metode itu akan mengembalikan, List<T>itu akan melempar pengecualian yang sama.
novigt

Saya mengerti maksud Anda dan sampai batas tertentu saya setuju - jika Anda menggunakan metode yang memberi Anda seorang IEnumerableemptor peringatan. Tetapi saya juga berpikir bahwa sebagai penulis metode, Anda memiliki tanggung jawab untuk mematuhi apa yang tersirat dari tanda tangan Anda. Metode ini dipanggil GetRecords, jadi ketika dikembalikan Anda akan mengharapkan untuk memiliki, Anda tahu, mendapatkan catatan . Jika disebut GiveMeSomethingICanPullRecordsFrom(atau, lebih realistis GetRecordSourceatau serupa) maka malas IEnumerableakan lebih dapat diterima.
Ben Aaronson

@ BenAaronson Saya setuju bahwa misalnya dalam File, ReadLinesdan ReadAllLinesdapat dengan mudah dibedakan dan itu hal yang baik. (Meskipun lebih karena fakta bahwa metode overload tidak dapat berbeda hanya dengan tipe return). Mungkin ReadRecords dan ReadAllRecords cocok di sini sebagai skema penamaan.
novigt

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.