การเชื่อมต่อฐานข้อมูลจะถูกปิดหรือไม่ถ้าเราให้ผลแถวของ data datader และไม่อ่านบันทึกทั้งหมด?


15

ในขณะที่ทำความเข้าใจวิธีการyieldทำงานของคำหลักฉันเจอlink1และlink2บน StackOverflow ซึ่งสนับสนุนการใช้งานyield returnในขณะวนซ้ำ DataReader และเหมาะสมกับความต้องการของฉันเช่นกัน แต่มันทำให้ฉันสงสัยว่าเกิดอะไรขึ้นถ้าฉันใช้yield returnดังที่แสดงด้านล่างและถ้าฉันไม่ทำซ้ำใน DataReader ทั้งหมดการเชื่อมต่อฐานข้อมูลจะเปิดตลอดไปหรือไม่

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

ฉันลองตัวอย่างเดียวกันในตัวอย่างแอปคอนโซลและสังเกตว่าในขณะที่การดีบั๊กที่บล็อกในที่สุด GetRecords()จะไม่ถูกดำเนินการ ฉันจะมั่นใจได้อย่างไรว่าการปิดการเชื่อมต่อฐานข้อมูล มีวิธีที่ดีกว่าการใช้yieldคำหลักหรือไม่ ฉันพยายามออกแบบคลาสที่กำหนดเองซึ่งจะรับผิดชอบการดำเนินการเลือก SQL และโพรซีเดอร์ที่เก็บไว้บน DB และจะส่งคืนผลลัพธ์ แต่ฉันไม่ต้องการคืน DataReader กลับไปยังผู้โทร นอกจากนี้ฉันต้องการตรวจสอบให้แน่ใจว่าการเชื่อมต่อจะถูกปิดในทุกสถานการณ์

แก้ไขเปลี่ยนคำตอบสำหรับคำตอบของเบ็นเนื่องจากไม่ถูกต้องที่ผู้โทรเมธอดจะใช้วิธีการอย่างถูกต้องและสำหรับการเชื่อมต่อฐานข้อมูลนั้นจะมีราคาแพงกว่าถ้าวิธีนั้นถูกเรียกหลายครั้งโดยไม่มีเหตุผล

ขอบคุณ 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 ตอนนี้มันจะเป็นค่าใช้จ่ายในหน่วยความจำหาก Datareader ของ SQLCommand ของฉันส่งคืนเอาต์พุตจำนวนมาก (เช่น myReader.NextResult ()) และฉันจะสร้างรายการของรายการหรือไม่ หรือส่งชุดข้อมูลไปยังผู้โทร [ส่วนตัวแล้วฉันไม่ชอบ] จะมีประสิทธิภาพมากขึ้นหรือไม่ [แต่การเชื่อมต่อจะเปิดได้นานขึ้น] สิ่งที่ฉันสับสนตรงนี้คือระหว่างการปิดการเชื่อมต่อไว้นานกว่าการสร้างรายการ คุณสามารถสรุปได้อย่างปลอดภัยว่าจะมีผู้เยี่ยมชมเว็บเพจของฉันมากกว่า 500 คนซึ่งเชื่อมต่อกับฐานข้อมูล
GawdePrasad

1
@GawdePrasad มันขึ้นอยู่กับสิ่งที่ผู้โทรจะทำกับผู้อ่าน ถ้ามันจะใส่ไอเท็มในคอลเล็กชันเองก็ไม่มีค่าใช้จ่ายที่สำคัญ หากการดำเนินการที่ไม่ต้องการเก็บค่าจำนวนมากในหน่วยความจำแสดงว่ามีหน่วยความจำโอเวอร์เฮด อย่างไรก็ตามหากคุณไม่ได้เก็บจำนวนมากมันอาจไม่ใช่ค่าใช้จ่ายที่คุณควรใส่ใจ ไม่ว่าจะด้วยวิธีใดก็ตามไม่ควรมีค่าใช้จ่ายสูง
Ben Aaronson

วิธี "พุชอย่าดึง" จะแก้ปัญหา "จนกว่าคุณจะวนซ้ำผลการค้นหาคุณจะยังคงเปิดการเชื่อมต่อ" การเชื่อมต่อยังคงเปิดอยู่จนกว่าคุณจะทำซ้ำได้แม้จะใช้วิธีนี้ใช่ไหม
BornToCode

1
@BontToCode ดูคำถาม: "ถ้าฉันใช้คืนผลตอบแทนตามที่แสดงด้านล่างและถ้าฉันไม่ทำซ้ำผ่าน DataReader ทั้งหมดการเชื่อมต่อฐานข้อมูลจะยังคงเปิดตลอดไป" ปัญหาคือคุณคืนค่า IEnumerable และผู้โทรทุกคนที่จัดการมันจะต้องระวัง gotcha พิเศษนี้: "ถ้าคุณไม่ทำซ้ำฉันแล้วฉันจะเปิดการเชื่อมต่อ" ในวิธีการพุชคุณจะต้องรับผิดชอบต่อผู้ที่โทรหาพวกเขาไม่มีตัวเลือกในการย้ำบางส่วน เมื่อการควบคุมเวลาถูกส่งกลับไปยังพวกเขาการเชื่อมต่อจะถูกปิด
Ben Aaronson

1
นี่คือคำตอบที่ยอดเยี่ยม ฉันชอบวิธีการพุชเพราะถ้าคุณมีคำถามที่ยิ่งใหญ่ที่คุณกำลังนำเสนอในแบบเพจคุณสามารถยกเลิกการอ่านก่อนหน้าสำหรับความเร็วโดยผ่าน Func <> แทนที่จะเป็น Action <>; สิ่งนี้ให้ความรู้สึกสะอาดกว่าการทำฟังก์ชั่น 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
    }
}

เพื่อให้การแจงนับจะถูกกำจัดแม้ว่าคุณจะออกจากวงด้วยหรือbreakreturn

เพื่อรับข้อมูลเพิ่มเติมเกี่ยวกับการใช้ตัววนซ้ำคุณสามารถอ่านบทความนี้โดย Jon Skeet

แก้ไข

ปัญหาเกี่ยวกับวิธีการดังกล่าวคือคุณต้องพึ่งพาลูกค้าในชั้นเรียนของคุณเพื่อใช้วิธีการอย่างถูกต้อง ตัวอย่างเช่นพวกเขาสามารถรับตัวแจงนับโดยตรงและวนซ้ำในwhileลูปโดยไม่ต้องกำจัด

คุณควรพิจารณาหนึ่งในโซลูชันที่แนะนำโดย @BenAaronson


1
สิ่งนี้ไม่น่าเชื่อถืออย่างยิ่ง ตัวอย่างเช่น: var records = GetRecords(); var first = records.First(); var count = records.Count()จะดำเนินการวิธีการนั้นสองครั้งโดยการเปิดและปิดการเชื่อมต่อและเครื่องอ่านในแต่ละครั้ง การวนซ้ำมันสองครั้งก็ทำสิ่งเดียวกัน คุณอาจส่งผ่านผลลัพธ์เป็นเวลานานก่อนที่จะวนซ้ำจริง ๆ เป็นต้นไม่มีสิ่งใดในวิธีการที่บ่งบอกถึงพฤติกรรมแบบนั้น
Ben Aaronson

2
@BenAaronson ฉันมุ่งเน้นไปที่คำถามเฉพาะเกี่ยวกับการใช้งานปัจจุบันในคำตอบของฉัน หากคุณมองปัญหาทั้งหมดฉันเห็นด้วยว่าควรใช้วิธีที่คุณแนะนำในคำตอบของคุณ
Jakub Lortz

1
@JakubLortz ยุติธรรมเพียงพอ
Ben Aaronson

2
@EsbenSkovPedersen โดยปกติเมื่อคุณพยายามทำบางสิ่งบางอย่างที่งี่เง่าคุณจะสูญเสียฟังก์ชันการทำงานบางอย่าง คุณต้องรักษาสมดุลเอาไว้ ที่นี่เราจะไม่สูญเสียอะไรมากนักโดยการโหลดผลลัพธ์อย่างกระตือรือร้น อย่างไรก็ตามปัญหาที่ใหญ่ที่สุดสำหรับฉันคือการตั้งชื่อ เมื่อฉันเห็นวิธีที่ชื่อการGetRecordsส่งคืนIEnumerableฉันคาดว่าจะโหลดเร็กคอร์ดไปยังหน่วยความจำ ในทางตรงกันข้ามถ้ามันเป็นคุณสมบัติRecordsฉันค่อนข้างจะคิดว่ามันเป็นนามธรรมบางอย่างผ่านฐานข้อมูล
Jakub Lortz

1
@EsbenSkovPedersen ดีเห็นความคิดเห็นของฉันในคำตอบของ nvoigt ฉันไม่มีปัญหาโดยทั่วไปกับวิธีการคืนค่า IEnumerable ที่จะทำงานได้อย่างมีนัยสำคัญเมื่อทำซ้ำ แต่ในกรณีนี้ดูเหมือนว่าจะไม่สอดคล้องกับความหมายของวิธีการลงนาม
Ben Aaronson

5

ไม่ว่าyieldพฤติกรรมของคุณจะเป็นอย่างไรรหัสของคุณจะมีเส้นทางการปฏิบัติงานที่จะไม่จัดการทรัพยากรของคุณอย่างถูกต้อง เกิดอะไรขึ้นถ้าบรรทัดที่สองของคุณส่งข้อยกเว้นหรือข้อที่สามของคุณ หรือแม้แต่สี่ของคุณ? คุณจะต้องลอง / ต่อท้ายที่ซับซ้อนมากหรือใช้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
การส่งคืน a IEnumerable<T> เป็นตัวบ่งชี้ว่าการแจกแจงสองครั้งจะส่งผลให้มีการโทรสองครั้ง คุณยังจะได้รับคำเตือนที่เป็นประโยชน์ในเครื่องมือของคุณหากคุณระบุจำนวนหลาย ๆ ครั้ง ประเด็นเกี่ยวกับการโยนข้อยกเว้นนั้นใช้ได้อย่างเท่าเทียมกันโดยไม่คำนึงถึงประเภทการคืนสินค้า ถ้าวิธีนั้นจะคืนค่า a List<T>มันจะส่งข้อยกเว้นเดียวกัน
nvoigt

ฉันเข้าใจประเด็นของคุณและเท่าที่ฉันเห็นด้วย - ถ้าคุณกำลังใช้วิธีการที่จะช่วยให้คุณเป็นผู้IEnumerableสังเกตการณ์แล้ว แต่ฉันก็คิดว่าในฐานะนักเขียนวิธีคุณมีความรับผิดชอบในการปฏิบัติตามสิ่งที่มีความหมายของคุณ วิธีการที่เรียกว่าGetRecordsดังนั้นเมื่อมันกลับที่คุณคาดหวังว่ามันจะมี y รู้อากาศระเบียน ถ้ามันถูกเรียกว่าGiveMeSomethingICanPullRecordsFrom(หรือมากกว่าGetRecordSourceหรือสมจริงกว่า) ก็IEnumerableจะเป็นที่ยอมรับได้มากกว่า
Ben Aaronson

@BenAaronson ผมยอมรับว่ายกตัวอย่างเช่นในFile, ReadLinesและReadAllLinesสามารถบอกได้อย่างง่ายดายจากกันและที่เป็นสิ่งที่ดี (แม้ว่าจะมีมากขึ้นเนื่องจากข้อเท็จจริงที่ว่าวิธีการโอเวอร์โหลดไม่สามารถแตกต่างกันตามประเภทการส่งคืนเท่านั้น) บางที ReadRecords และ ReadAllRecords จะพอดีที่นี่เป็นรูปแบบการตั้งชื่อเช่นกัน
nvoigt

2

สิ่งที่คุณทำผิดไป แต่จะทำงานได้ดี

คุณควรใช้IEnumerator <>เนื่องจากยังสืบทอดIDisposable IDisposable

แต่เนื่องจากคุณใช้ foreach คอมไพเลอร์ยังคงใช้IDisposableในการสร้างIEnumerator <>สำหรับ foreach

โดยข้อเท็จจริงแล้ว foreach เกี่ยวข้องกับสิ่งต่าง ๆ มากมาย

ตรวจสอบ/programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-use

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.