SqlException จาก Entity Framework - ไม่อนุญาตให้ทำธุรกรรมใหม่เนื่องจากมีเธรดอื่นที่ทำงานอยู่ในเซสชัน


600

ขณะนี้ฉันได้รับข้อผิดพลาดนี้:

System.Data.SqlClient.SqlException: ไม่อนุญาตการทำธุรกรรมใหม่เนื่องจากมีเธรดอื่นที่ทำงานอยู่ในเซสชัน

ขณะเรียกใช้รหัสนี้:

public class ProductManager : IProductManager
{
    #region Declare Models
    private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
    private RivWorks.Model.NegotiationAutos.RivFeedsEntities _dbFeed = RivWorks.Model.Stores.FeedEntities(AppSettings.FeedAutosEntities_connString);
    #endregion

    public IProduct GetProductById(Guid productId)
    {
        // Do a quick sync of the feeds...
        SyncFeeds();
        ...
        // get a product...
        ...
        return product;
    }

    private void SyncFeeds()
    {
        bool found = false;
        string feedSource = "AUTO";
        switch (feedSource) // companyFeedDetail.FeedSourceTable.ToUpper())
        {
            case "AUTO":
                var clientList = from a in _dbFeed.Client.Include("Auto") select a;
                foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
                {
                    var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
                    foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                    {
                        if (companyFeedDetail.FeedSourceTable.ToUpper() == "AUTO")
                        {
                            var company = (from a in _dbRiv.Company.Include("Product") where a.CompanyId == companyFeedDetail.CompanyId select a).First();
                            foreach (RivWorks.Model.NegotiationAutos.Auto sourceProduct in client.Auto)
                            {
                                foreach (RivWorks.Model.Negotiation.Product targetProduct in company.Product)
                                {
                                    if (targetProduct.alternateProductID == sourceProduct.AutoID)
                                    {
                                        found = true;
                                        break;
                                    }
                                }
                                if (!found)
                                {
                                    var newProduct = new RivWorks.Model.Negotiation.Product();
                                    newProduct.alternateProductID = sourceProduct.AutoID;
                                    newProduct.isFromFeed = true;
                                    newProduct.isDeleted = false;
                                    newProduct.SKU = sourceProduct.StockNumber;
                                    company.Product.Add(newProduct);
                                }
                            }
                            _dbRiv.SaveChanges();  // ### THIS BREAKS ### //
                        }
                    }
                }
                break;
        }
    }
}

รุ่น # 1 - รุ่นนี้ตั้งอยู่ในฐานข้อมูลบน Dev Server ของเรา รุ่น # 1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0-a7a1-2bb6b05d8bc1/Model1.png

รุ่น # 2 - รุ่นนี้ตั้งอยู่ในฐานข้อมูลบนเซิร์ฟเวอร์ Prod ของเราและอัพเดททุกวันด้วยฟีดอัตโนมัติ alt text http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/4260259f-bce6-43d5-9d2a-017bd9a980d4/Model2.png

หมายเหตุ - ไอเท็มวงกลมสีแดงในรุ่น # 1 เป็นฟิลด์ที่ฉันใช้เพื่อ "แมป" กับโมเดล # 2 โปรดเพิกเฉยวงกลมสีแดงในแบบจำลอง # 2: นั่นมาจากคำถามอื่นที่ฉันได้รับคำตอบแล้ว

หมายเหตุ: ฉันยังคงต้องใส่การตรวจสอบ isDeleted เพื่อให้ฉันสามารถลบมันได้อย่างนุ่มนวลจาก DB1 ถ้ามันหายไปจากสินค้าคงคลังของลูกค้าของเรา

สิ่งที่ฉันต้องการทำด้วยรหัสเฉพาะนี้คือการเชื่อมต่อ บริษัท ใน DB1 กับลูกค้าใน DB2 รับรายการผลิตภัณฑ์จาก DB2 และ INSERT ใน DB1 หากยังไม่ได้มี ครั้งแรกที่ผ่านควรมีการดึงสินค้าคงคลังอย่างเต็มรูปแบบ ทุกครั้งที่เรียกใช้ที่นั่นหลังจากไม่มีสิ่งใดเกิดขึ้นเว้นแต่ว่ามีคลังโฆษณาใหม่เข้ามาในฟีดข้ามคืน

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


6
นี่เป็นคำถามที่ละเอียดที่สุดที่ฉันเคยเห็น

9
ทุกคนคิดถึงขั้นตอนการจัดเก็บหรือยัง
เดวิด

คำตอบ:


690

หลังจากดึงผมออกมามากฉันค้นพบว่าforeachลูปเป็นตัวการ สิ่งที่ต้องเกิดขึ้นคือโทรหา EF แต่ให้กลับไปเป็นIList<T>ประเภทเป้าหมายนั้นแล้ววนIList<T>กลับไปที่

ตัวอย่าง:

IList<Client> clientList = from a in _dbFeed.Client.Include("Auto") select a;
foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
{
   var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
    // ...
}

14
ใช่สิ่งนี้ทำให้ฉันปวดหัวด้วย ฉันเกือบตกเก้าอี้เมื่อพบปัญหา! ฉันเข้าใจเหตุผลทางเทคนิคที่อยู่เบื้องหลังปัญหา แต่สิ่งนี้ไม่ง่ายและไม่ได้ช่วยนักพัฒนาให้ตกอยู่ใน "จุดแห่งความสำเร็จ" blogs.msdn.com/brada/archive/2003/10/02/50420 aspx
หมอโจนส์

9
การใช้ชุดข้อมูลขนาดใหญ่นั้นมีประสิทธิภาพหรือไม่ หากคุณมีบันทึกนับล้านในตาราง ToList () จะดูดข้อมูลเหล่านั้นทั้งหมดไว้ในหน่วยความจำ ฉันพบปัญหานี้มากและสงสัยว่าสิ่งต่อไปนี้จะเป็นไปได้หรือไม่ a) ถอดเอนทิตี b) สร้าง ObjectContext ใหม่และแนบเอนทิตีที่แยกออกไป c) Call SaveChanges () บน ObjectContext ใหม่ d) แยกเอนทิตีจาก ObjectContext ใหม่ e) แนบกลับไปที่ ObjectContext เก่า
Abhijeet Patel

149
ปัญหาคือคุณไม่สามารถโทรได้SaveChangesในขณะที่คุณยังดึงผลลัพธ์จากฐานข้อมูล ดังนั้นวิธีแก้ไขปัญหาอื่นก็เพื่อบันทึกการเปลี่ยนแปลงเมื่อลูปเสร็จสมบูรณ์
Drew Noakes

4
เมื่อถูกกัดฉันก็เพิ่มสิ่งนี้ลงใน Microsoft Connect: connect.microsoft.com/VisualStudio/feedback/details/612369/… รู้สึกอิสระที่จะลงคะแนน
Ian Mercer

36
ผู้พัฒนาของเรามักจะผนวก. ToList () ลงในเคียวรี LINQ ใด ๆ โดยไม่ต้องนึกถึงผลลัพธ์ นี่จะเป็นครั้งแรกที่ต่อท้าย. ToList () มีประโยชน์จริง ๆ !
Marc

267

ตามที่คุณได้ระบุไว้แล้วคุณไม่สามารถบันทึกจากภายในforeachที่ยังคงวาดจากฐานข้อมูลผ่านทางเครื่องอ่านที่ใช้งาน

การโทรToList()หรือToArray()ดีสำหรับชุดข้อมูลขนาดเล็ก แต่เมื่อคุณมีหลายพันแถวคุณจะต้องใช้หน่วยความจำจำนวนมาก

จะดีกว่าถ้าโหลดแถวเป็นกลุ่ม

public static class EntityFrameworkUtil
{
    public static IEnumerable<T> QueryInChunksOf<T>(this IQueryable<T> queryable, int chunkSize)
    {
        return queryable.QueryChunksOfSize(chunkSize).SelectMany(chunk => chunk);
    }

    public static IEnumerable<T[]> QueryChunksOfSize<T>(this IQueryable<T> queryable, int chunkSize)
    {
        int chunkNumber = 0;
        while (true)
        {
            var query = (chunkNumber == 0)
                ? queryable 
                : queryable.Skip(chunkNumber * chunkSize);
            var chunk = query.Take(chunkSize).ToArray();
            if (chunk.Length == 0)
                yield break;
            yield return chunk;
            chunkNumber++;
        }
    }
}

ด้วยวิธีการส่วนขยายข้างต้นคุณสามารถเขียนแบบสอบถามของคุณดังนี้:

foreach (var client in clientList.OrderBy(c => c.Id).QueryInChunksOf(100))
{
    // do stuff
    context.SaveChanges();
}

วัตถุที่สามารถสืบค้นได้ที่คุณเรียกใช้เมธอดนี้ต้องสั่งซื้อ นี่เป็นเพราะเอนทิตีกรอบงานสนับสนุนเฉพาะIQueryable<T>.Skip(int)ในแบบสอบถามที่สั่งซื้อซึ่งทำให้รู้สึกเมื่อคุณพิจารณาว่าแบบสอบถามหลายรายการสำหรับช่วงที่แตกต่างกันต้องการให้การสั่งซื้อมีเสถียรภาพ หากการสั่งซื้อนั้นไม่สำคัญสำหรับคุณเพียงแค่สั่งซื้อด้วยรหัสหลักเนื่องจากเป็นไปได้ว่าจะมีดัชนีแบบกลุ่ม

รุ่นนี้จะสอบถามฐานข้อมูลเป็นกลุ่ม 100 หมายเหตุที่SaveChanges()เรียกว่าสำหรับแต่ละเอนทิตี

ถ้าคุณต้องการปรับปรุงทรูพุตของคุณอย่างมากคุณควรโทรหาSaveChanges()น้อยลง ใช้รหัสเช่นนี้แทน:

foreach (var chunk in clientList.OrderBy(c => c.Id).QueryChunksOfSize(100))
{
    foreach (var client in chunk)
    {
        // do stuff
    }
    context.SaveChanges();
}

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

และได้รับการยกเว้นที่คุณเห็น

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

การวนซ้ำครั้งแรกไม่จำเป็นต้องข้ามอะไรเลยง่ายกว่า

SELECT TOP (100)                     -- the chunk size 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
FROM [dbo].[Clients] AS [Extent1]
ORDER BY [Extent1].[Id] ASC

การโทรครั้งต่อไปจะต้องข้ามผลลัพธ์ก่อนหน้าดังนั้นแนะนำการใช้งานของrow_number:

SELECT TOP (100)                     -- the chunk size
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
FROM (
    SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], row_number()
    OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
    FROM [dbo].[Clients] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 100   -- the number of rows to skip
ORDER BY [Extent1].[Id] ASC

17
ขอบคุณ คำอธิบายของคุณมีประโยชน์มากกว่าคำตอบที่ทำเครื่องหมายว่า "ตอบแล้ว"
Wagner da Silva

1
มันเยี่ยมมาก มีเพียงสิ่งเดียว: หากคุณกำลังสอบถามในคอลัมน์และอัปเดตค่าของคอลัมน์นั้นคุณจะต้องเป็นพัสดุของ chunkNumber ++; . สมมติว่าคุณมีคอลัมน์ "ModifiedDate" และคุณกำลังค้นหา. Where (x => x.ModifiedDate! = null) และในตอนท้ายของแต่ละครั้งคุณจะตั้งค่าสำหรับ ModifiedDate ด้วยวิธีนี้คุณจะไม่ทำซ้ำครึ่งหนึ่งของระเบียนเพราะครึ่งหนึ่งของระเบียนกำลังถูกข้าม
Arvand

แต่น่าเสียดายที่ในชุดข้อมูลขนาดใหญ่ที่คุณจะได้คำอธิบาย OutOfMemoryException -See ใน กรอบ Entity ชุดข้อมูลขนาดใหญ่ออกมาจากข้อยกเว้นหน่วยความจำ ฉันอธิบายวิธีการต่ออายุบริบทของคุณแต่ละชุดในSqlException จาก Entity Framework - ไม่อนุญาตการทำธุรกรรมใหม่เนื่องจากมีเธรดอื่นที่ทำงานอยู่ในเซสชัน
Michael Freidgeim

ฉันคิดว่ามันน่าจะใช้ได้นะ var skip = 0; const ใช้เวลา = 100; รายการ <Employee> emps; ในขณะที่ ((emps = db.Employees.Skip (ข้าม). Take (take) .ToList ()). Count> 0) {skip + = take; foreach (var emp in emps) {// ทำสิ่งที่นี่}} ฉันจะกำหนดคำตอบนี้ แต่มันจะถูกฝังอยู่ใต้กองคำตอบด้านล่างและเกี่ยวข้องกับคำถามนี้
jwize

123

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

ข้อผิดพลาดนี้เกิดจาก Entity Framework สร้างธุรกรรมโดยนัยระหว่างการโทร SaveChanges () วิธีที่ดีที่สุดในการแก้ไขข้อผิดพลาดคือใช้รูปแบบที่แตกต่างกัน (เช่นไม่บันทึกในขณะที่อ่าน) หรือโดยประกาศธุรกรรมอย่างชัดเจน วิธีแก้ปัญหาที่เป็นไปได้สามข้อต่อไปนี้:

// 1: Save after iteration (recommended approach in most cases)
using (var context = new MyContext())
{
    foreach (var person in context.People)
    {
        // Change to person
    }
    context.SaveChanges();
}

// 2: Declare an explicit transaction
using (var transaction = new TransactionScope())
{
    using (var context = new MyContext())
    {
        foreach (var person in context.People)
        {
            // Change to person
            context.SaveChanges();
        }
    }
    transaction.Complete();
}

// 3: Read rows ahead (Dangerous!)
using (var context = new MyContext())
{
    var people = context.People.ToList(); // Note that this forces the database
                                          // to evaluate the query immediately
                                          // and could be very bad for large tables.

    foreach (var person in people)
    {
        // Change to person
        context.SaveChanges();
    }
} 

6
หากคุณใช้เส้นทางการทำธุรกรรมเพียงแค่ขว้างใน TransactionScope อาจไม่สามารถแก้ไขได้ - อย่าลืมที่จะขยายการหมดเวลาหากสิ่งที่คุณทำอาจใช้เวลานาน - ตัวอย่างเช่นหากคุณกำลังทำการดีบักโค้ดแบบโต้ตอบ การเรียก DB นี่คือโค้ดที่ขยายการหมดเวลาของธุรกรรมเป็นหนึ่งชั่วโมง: การใช้ (var transaction = new TransactionScope (TransactionScopeOption.Required, TimeSpan ใหม่ (1, 0, 0)))
Chris Moschini

ฉันชนกับข้อผิดพลาดนี้เป็นครั้งแรกที่ฉันพูดนอกเรื่อง "เส้นทางการสอน" เป็นตัวอย่างจริงด้วยตัวเอง! อย่างไรก็ตามสำหรับฉันแล้ววิธีแก้ปัญหาที่ง่ายกว่าคือประหยัดหลังจากที่ทำซ้ำดีกว่า! (ฉันคิดว่า 99% ของเวลาเป็นกรณีนี้และมีเพียง 1% เท่านั้นที่ต้องดำเนินการในฐานข้อมูลเพื่อบันทึกการวนรอบ)
เดอร์แมน

ทั้งหมด ฉันเพิ่งชนกับข้อผิดพลาดนี้ น่ารังเกียจมาก ข้อเสนอแนะที่สองทำงานเหมือนมีเสน่ห์สำหรับฉันพร้อมกับย้าย SaveChanges ของฉันไปที่ลูป ฉันคิดว่าการบันทึกการเปลี่ยนแปลงภายนอกลูปนั้นดีกว่าสำหรับการเปลี่ยนแปลงแบบแบ็ตช์ แต่ไม่เป็นไร ฉันเดาว่าไม่?! :(
นายยัง

ไม่ทำงานสำหรับฉัน. NET 4.5 เมื่อใช้ TransactionScope ฉันได้รับข้อผิดพลาดดังต่อไปนี้ "ผู้ให้บริการที่อยู่ภายใต้ล้มเหลวใน EnlistTransaction {" ผู้จัดการธุรกรรมคู่ค้าได้ปิดการสนับสนุนสำหรับธุรกรรมระยะไกล / เครือข่าย (ข้อยกเว้นจาก HRESULT: 0x8004D025) "}" ฉันสิ้นสุดการทำงานนอกการทำซ้ำ
Diganta Kumar

การใช้ TransactionScope เป็นสิ่งที่อันตรายเนื่องจากตารางถูกล็อคตลอดเวลาของการทำธุรกรรมทั้งหมด
Michael Freidgeim

19

แน่นอนคุณไม่สามารถบันทึกการเปลี่ยนแปลงภายในforeachลูปใน C # โดยใช้ Entity Framework

context.SaveChanges() วิธีการทำหน้าที่เหมือนกระทำในระบบฐานข้อมูลปกติ (RDMS)

เพียงทำการเปลี่ยนแปลงทั้งหมด (Entity Framework ใดที่จะแคช) จากนั้นให้บันทึกทั้งหมดในครั้งเดียวSaveChanges()หลังจากเรียกลูป (ด้านนอกของมัน) เช่นฐานข้อมูลคำสั่งการกระทำ

มันจะทำงานถ้าคุณสามารถบันทึกการเปลี่ยนแปลงทั้งหมดในครั้งเดียว


2
ฉันคิดว่ามันน่าสนใจที่จะเห็น "ระบบฐานข้อมูลปกติ (RDMS)" ที่นี่
Dinerdo

1
ดูเหมือนว่าผิดเนื่องจากการโทร SaveChanges ซ้ำ ๆ เป็นเรื่องปกติใน 90% ของบริบทใน EF
Pxtl

ดูเหมือนกับว่าการเรียก SaveChanges ซ้ำ ๆ เป็นปกติเว้นแต่ว่า foreach loop จะทำซ้ำผ่าน db Entity
kerbasaurus

1
Aha! นำบริบทภายในสำหรับแต่ละวง! (pffft ... ฉันกำลังคิดอะไรอยู่? .. ) ขอบคุณ!
Adam Cox

18

เพียงแค่ใส่context.SaveChanges()หลังของคุณforeach(วน)


นี่เป็นตัวเลือกที่ดีกว่าที่ฉันพบในกรณีของฉันเนื่องจากบันทึกไว้ข้างหน้า
Almeida

2
นี่ไม่ใช่ตัวเลือกเสมอไป
Pxtl

9

ใช้การเลือกของคุณเป็นรายการเสมอ

เช่น:

var tempGroupOfFiles = Entities.Submited_Files.Where(r => r.FileStatusID == 10 && r.EventID == EventId).ToList();

จากนั้นวนรอบคอลเลกชันในขณะที่บันทึกการเปลี่ยนแปลง

 foreach (var item in tempGroupOfFiles)
             {
                 var itemToUpdate = item;
                 if (itemToUpdate != null)
                 {
                     itemToUpdate.FileStatusID = 8;
                     itemToUpdate.LastModifiedDate = DateTime.Now;
                 }
                 Entities.SaveChanges();

             }

1
นี่ไม่ใช่การฝึกฝนที่ดีเลย คุณไม่ควรเรียกใช้ SaveChanges ซึ่งบ่อยครั้งหากคุณไม่ต้องการใช้และคุณไม่ควร "ใช้รายการที่คุณเลือกเป็นรายการ"
เสมอ

@Dinerdo มันขึ้นอยู่กับสถานการณ์ ในกรณีของฉันฉันมี 2 ห่วงด้านหน้า ด้านนอกมีแบบสอบถาม db เป็นรายการ ตัวอย่างเช่นอุปกรณ์นี้จะทำการสำรวจอุปกรณ์ฮาร์ดแวร์ Inner foreach ดึงข้อมูลหลายอย่างจากแต่ละอุปกรณ์ ตามความต้องการฉันต้องบันทึกลงในฐานข้อมูลหลังจากดึงข้อมูลจากอุปกรณ์แต่ละตัวทีละตัว ไม่มีตัวเลือกในการบันทึกข้อมูลทั้งหมดในตอนท้ายของกระบวนการ ฉันพบข้อผิดพลาดเดียวกัน แต่วิธีการแก้ปัญหาของ mzonerz ทำงานได้
jstuardo

@ jstuardo แม้จะมีการแบทช์?
Dinerdo

@Dinerdo ฉันยอมรับว่ามันไม่ใช่วิธีปฏิบัติที่ดีในระดับปรัชญา อย่างไรก็ตามมีหลายสถานการณ์ที่ภายในโค้ดวนรอบเรียกวิธีอื่น (สมมติว่าเป็นวิธี AddToLog ()) ซึ่งรวมถึงการเรียกไปยัง db.SaveChanges () แบบโลคัล ในสถานการณ์นี้คุณไม่สามารถควบคุมการเรียกไปยัง db.Save Changes ได้ ในกรณีนี้การใช้ ToList () หรือโครงสร้างที่คล้ายกันจะทำงานตามที่แนะนำโดย mzonerz ขอบคุณ!
A. Varma

ในทางปฏิบัติสิ่งนี้จะทำร้ายคุณมากกว่าจะช่วย ฉันสนับสนุนสิ่งที่ฉันพูด - ToList () ไม่ควรใช้ตลอดเวลาและการบันทึกการเปลี่ยนแปลงหลังจากทุกรายการเป็นสิ่งที่ควรหลีกเลี่ยงทุกที่ที่เป็นไปได้ในแอปประสิทธิภาพสูง นี่จะเป็นการแก้ไขชั่วคราวของ IMO ไม่ว่าคุณจะใช้วิธีการบันทึกแบบใดควรใช้ประโยชน์จากการบัฟเฟอร์
Dinerdo

8

FYI: จากหนังสือและบางบรรทัดถูกปรับเนื่องจากสไตล์ไม่ถูกต้อง:

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

หากคุณพยายามเรียกใช้ SaveChanges () ก่อนที่จะประมวลผลข้อมูลทั้งหมดคุณจะได้รับข้อยกเว้น "ธุรกรรมใหม่ไม่ได้รับอนุญาตเนื่องจากมีเธรดอื่นทำงานอยู่ในเซสชัน" ข้อยกเว้นเกิดขึ้นเนื่องจาก SQL Server ไม่อนุญาตให้เริ่มต้นธุรกรรมใหม่ในการเชื่อมต่อที่มี SqlDataReader เปิดอยู่แม้ว่าจะมีการเปิดใช้งานหลายชุดระเบียน (MARS) โดยสตริงการเชื่อมต่อ (สตริงการเชื่อมต่อเริ่มต้นของ EF ทำให้ MARS)

บางครั้งดีกว่าที่จะเข้าใจว่าทำไมสิ่งที่เกิดขึ้น ;-)


1
วิธีที่ดีในการหลีกเลี่ยงปัญหานี้คือเมื่อคุณเปิดเครื่องอ่านเพื่อเปิดเครื่องที่สอง นี่คือสิ่งที่คุณต้องการเมื่อคุณอัปเดตข้อมูลหลัก / รายละเอียดในกรอบงานเอนทิตี คุณเปิดการเชื่อมต่อแรกสำหรับระเบียนหลักและรายการที่สองสำหรับระเบียนรายละเอียด หากคุณเป็นคนอ่านเท่านั้นไม่น่าจะมีปัญหา ปัญหาเกิดขึ้นระหว่างการอัพเดต
Herman Van Der Blom

คำอธิบายที่เป็นประโยชน์ คุณพูดถูกมันเป็นการดีที่จะเข้าใจว่าทำไมสิ่งต่าง ๆ ถึงเกิดขึ้น
Dov Miller

8

ทำรายการที่สามารถสืบค้นได้ของคุณเป็น. ToList () และควรจะทำงานได้ดี


1
โปรดให้ตัวอย่างแทนที่จะเพิ่งโพสต์โซลูชัน
Ronnie Oosting

5

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


3
มีปัญหาที่คล้ายกันเมื่อเร็ว ๆ นี้: เหตุผลในกรณีของฉันคือSELECTคำสั่งในกระบวนงานที่เก็บไว้ซึ่งสร้างชุดผลลัพธ์ว่างเปล่าและหากไม่ได้อ่านชุดผลลัพธ์ให้SaveChangesโยนข้อยกเว้นนั้น
n0rd

สิ่งเดียวกันกับผลยังไม่ได้อ่านจาก SP ขอบคุณมากสำหรับคำแนะนำ)
พาเวลเค

4

ต่อไปนี้เป็นอีก 2 ตัวเลือกที่อนุญาตให้คุณเรียกใช้ SaveChanges () ในแต่ละลูป

ตัวเลือกแรกคือใช้หนึ่ง DBContext เพื่อสร้างรายการวัตถุของคุณเพื่อวนซ้ำแล้วสร้าง DBContext ที่สองเพื่อเรียกใช้ SaveChanges () นี่คือตัวอย่าง:

//Get your IQueryable list of objects from your main DBContext(db)    
IQueryable<Object> objects = db.Object.Where(whatever where clause you desire);

//Create a new DBContext outside of the foreach loop    
using (DBContext dbMod = new DBContext())
{   
    //Loop through the IQueryable       
    foreach (Object object in objects)
    {
        //Get the same object you are operating on in the foreach loop from the new DBContext(dbMod) using the objects id           
        Object objectMod = dbMod.Object.Find(object.id);

        //Make whatever changes you need on objectMod
        objectMod.RightNow = DateTime.Now;

        //Invoke SaveChanges() on the dbMod context         
        dbMod.SaveChanges()
    }
}

ตัวเลือกที่สองคือการรับรายการของวัตถุฐานข้อมูลจาก DBContext แต่เพื่อเลือกเฉพาะ ID จากนั้นวนซ้ำรายการ id (สันนิษฐานว่าเป็น int) และรับวัตถุที่สอดคล้องกับแต่ละ int และเรียกใช้ SaveChanges () ด้วยวิธีนั้น แนวคิดเบื้องหลังเมธอดนี้กำลังดึงรายการจำนวนเต็มจำนวนมากมีประสิทธิภาพมากขึ้นจากนั้นรับรายการ db วัตถุขนาดใหญ่และเรียก. ToList () บนวัตถุทั้งหมด นี่คือตัวอย่างของวิธีนี้:

//Get the list of objects you want from your DBContext, and select just the Id's and create a list
List<int> Ids = db.Object.Where(enter where clause here)Select(m => m.Id).ToList();

var objects = Ids.Select(id => db.Objects.Find(id));

foreach (var object in objects)
{
    object.RightNow = DateTime.Now;
    db.SaveChanges()
}

นี่เป็นทางเลือกที่ยอดเยี่ยมที่ฉันคิดและทำ แต่สิ่งนี้จำเป็นต้องได้รับการสนับสนุน หมายเหตุ: i) คุณสามารถทำซ้ำได้ซึ่งนับว่าดีสำหรับชุดที่มีขนาดใหญ่มาก ii) คุณสามารถใช้คำสั่ง NoTracking เพื่อหลีกเลี่ยงปัญหาในการโหลดบันทึกจำนวนมาก (ถ้านั่นเป็นสถานการณ์ของคุณ); iii) ฉันชอบตัวเลือกคีย์หลักเท่านั้นเช่นกัน - มันฉลาดมากเพราะคุณโหลดข้อมูลลงในหน่วยความจำน้อยลงมาก แต่คุณไม่ได้เกี่ยวข้องกับ Take / Skip บนชุดข้อมูลต้นแบบที่มีศักยภาพแบบไดนามิก
ทอดด์

4

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

ตัวอย่างเช่น

    using (var context = new DatabaseContext())
    {
        ...
        using (var context1 = new DatabaseContext())
        {
            ...
               context1.SaveChanges();
        }                         
        //get id of inserted object from context1 and use is.   
      context.SaveChanges();
   }

2

ดังนั้นในโครงการฉันมีปัญหาเดียวกันนี้อย่างแน่นอนปัญหาไม่ได้อยู่ในนั้นforeachหรือ.toList()เป็นจริงในการกำหนดค่า AutoFac ที่เราใช้ สิ่งนี้สร้างสถานการณ์แปลก ๆ บางอย่างให้เกิดข้อผิดพลาดด้านบน แต่ยังโยนข้อผิดพลาดอื่น ๆ ที่เทียบเท่ากัน

นี่คือการแก้ไขของเรา: เปลี่ยนสิ่งนี้:

container.RegisterType<DataContext>().As<DbContext>().InstancePerLifetimeScope();
container.RegisterType<DbFactory>().As<IDbFactory>().SingleInstance();
container.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();

ถึง:

container.RegisterType<DataContext>().As<DbContext>().As<DbContext>();
container.RegisterType<DbFactory>().As<IDbFactory>().As<IDbFactory>().InstancePerLifetimeScope();
container.RegisterType<UnitOfWork>().As<IUnitOfWork>().As<IUnitOfWork>();//.InstancePerRequest();

คุณช่วยอธิบายสิ่งที่คุณคิดว่าเป็นปัญหาได้อย่างละเอียดหรือไม่ คุณแก้ไขปัญหานี้ด้วยการสร้าง Dbcontext ใหม่ทุกครั้งหรือไม่
eran otzap

2

ฉันรู้ว่ามันเป็นคำถามเก่า แต่ฉันพบข้อผิดพลาดวันนี้

และฉันพบว่าข้อผิดพลาดนี้สามารถโยนได้เมื่อทริกเกอร์ตารางฐานข้อมูลได้รับข้อผิดพลาด

สำหรับข้อมูลของคุณคุณสามารถตรวจสอบทริกเกอร์ตารางของคุณได้เช่นกันเมื่อคุณได้รับข้อผิดพลาดนี้


2

ฉันต้องการอ่าน ResultSet ขนาดใหญ่และอัปเดตบางระเบียนในตาราง ฉันพยายามใช้ชิ้นตามคำแนะนำใน คำตอบของDrew Noakesคำตอบ

น่าเสียดายหลังจากบันทึก 50,000 รายการฉันได้รับ OutofMemoryException คำตอบEntity framework ชุดข้อมูลขนาดใหญ่ซึ่งเกินความจำจะอธิบายได้ว่า

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

คำแนะนำคือการสร้างบริบทของคุณอีกครั้งสำหรับแต่ละชุด

ดังนั้นฉันจึงดึงค่า Minimal และ Maximum ของคีย์หลัก - ตารางมีคีย์หลักเป็นจำนวนเต็มส่วนเพิ่มอัตโนมัติจากนั้นฉันดึงข้อมูลจากฐานข้อมูลของระเบียนโดยเปิดบริบทสำหรับแต่ละอัน หลังจากประมวลผลบริบท chunk ปิดและปล่อยหน่วยความจำ มั่นใจได้ว่าการใช้งานหน่วยความจำจะไม่เติบโต

ด้านล่างนี้เป็นตัวอย่างข้อมูลจากรหัสของฉัน:

  public void ProcessContextByChunks ()
  {
        var tableName = "MyTable";
         var startTime = DateTime.Now;
        int i = 0;
         var minMaxIds = GetMinMaxIds();
        for (int fromKeyID= minMaxIds.From; fromKeyID <= minMaxIds.To; fromKeyID = fromKeyID+_chunkSize)
        {
            try
            {
                using (var context = InitContext())
                {   
                    var chunk = GetMyTableQuery(context).Where(r => (r.KeyID >= fromKeyID) && (r.KeyID < fromKeyID+ _chunkSize));
                    try
                    {
                        foreach (var row in chunk)
                        {
                            foundCount = UpdateRowIfNeeded(++i, row);
                        }
                        context.SaveChanges();
                    }
                    catch (Exception exc)
                    {
                        LogChunkException(i, exc);
                    }
                }
            }
            catch (Exception exc)
            {
                LogChunkException(i, exc);
            }
        }
        LogSummaryLine(tableName, i, foundCount, startTime);
    }

    private FromToRange<int> GetminMaxIds()
    {
        var minMaxIds = new FromToRange<int>();
        using (var context = InitContext())
        {
            var allRows = GetMyTableQuery(context);
            minMaxIds.From = allRows.Min(n => (int?)n.KeyID ?? 0);  
            minMaxIds.To = allRows.Max(n => (int?)n.KeyID ?? 0);
        }
        return minMaxIds;
    }

    private IQueryable<MyTable> GetMyTableQuery(MyEFContext context)
    {
        return context.MyTable;
    }

    private  MyEFContext InitContext()
    {
        var context = new MyEFContext();
        context.Database.Connection.ConnectionString = _connectionString;
        //context.Database.Log = SqlLog;
        return context;
    }

FromToRangeเป็นโครงสร้างอย่างง่ายพร้อมคุณสมบัติ From and To


ฉันไม่เห็นว่าคุณ "ต่ออายุ" บริบทของคุณอย่างไร ดูเหมือนว่าคุณกำลังสร้างบริบทใหม่ให้กับแต่ละอัน
Suncat2000

@ Suncat2000 คุณมีสิทธิที่ควรจะเป็นบริบทชีวิตสั้นวัตถุstackoverflow.com/questions/43474112/...
ไมเคิล Freidgeim

1

ฉันกำลังเผชิญกับปัญหาเดียวกัน

นี่คือสาเหตุและวิธีแก้ไข

http://blogs.msdn.com/b/cbiyikoglu/archive/2006/11/21/mars-transactions-and-sql-error-3997-3988-or-3983.aspx

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

ข้อผิดพลาดทั่วไปส่วนใหญ่คือฟังก์ชั่นที่อ่านข้อมูลจาก db และคืนค่า สำหรับฟังก์ชั่นเช่นเช่น isRecordExist

ในกรณีนี้เรากลับทันทีจากฟังก์ชั่นถ้าเราพบบันทึกและลืมปิดผู้อ่าน


7
"close a reader" หมายถึงอะไรใน Entity Framework? ไม่มีตัวอ่านที่มองเห็นได้ในแบบสอบถามเช่น var result = จากลูกค้าใน myDb.Customers ที่ customer.Id == customerId เลือกลูกค้า; ผลตอบแทนที่ได้ครั้งแรก OrderDefault ();
Anthony

@Anony ตามคำตอบอื่น ๆ ถ้าคุณใช้ EF เพื่อแจกแจงการสืบค้น LINQ (IQueryable) DataReader ที่แฝงอยู่จะยังคงเปิดอยู่จนกว่าจะมีการทำซ้ำแถวสุดท้าย แต่ถึงแม้ว่า MARS จะเป็นคุณสมบัติที่สำคัญในการเปิดใช้งานในสตริงการเชื่อมต่อปัญหาใน OP ยังไม่สามารถแก้ไขได้ด้วย MARS เพียงอย่างเดียว ปัญหาพยายาม SaveChanges ในขณะที่ DataReader พื้นฐานยังคงเปิดอยู่
ทอดด์

1

รหัสด้านล่างใช้งานได้สำหรับฉัน:

private pricecheckEntities _context = new pricecheckEntities();

...

private void resetpcheckedtoFalse()
{
    try
    {
        foreach (var product in _context.products)
        {
            product.pchecked = false;
            _context.products.Attach(product);
            _context.Entry(product).State = EntityState.Modified;
        }
        _context.SaveChanges();
    }
    catch (Exception extofException)
    {
        MessageBox.Show(extofException.ToString());

    }
    productsDataGrid.Items.Refresh();
}

2
ยินดีต้อนรับสู่ SO! ลองเพิ่มคำอธิบายและ / หรือลิงก์ที่อธิบายว่าทำไมสิ่งนี้ถึงได้ผลสำหรับคุณ คำตอบที่ใช้รหัสเท่านั้นโดยทั่วไปถือว่าไม่ดีสำหรับ SO
codeMagic

1

ในกรณีของฉันปัญหาปรากฏขึ้นเมื่อฉันเรียกว่า Stored Procedure ผ่านทาง EF จากนั้น SaveChanges ภายหลังเปลี่ยนข้อยกเว้นนี้ ปัญหาเกิดขึ้นในขั้นตอนการเรียกใช้ตัวแจงนับไม่ได้ถูกกำจัด ฉันแก้ไขรหัสต่อไปนี้:

public bool IsUserInRole(string username, string roleName, DataContext context)
{          
   var result = context.aspnet_UsersInRoles_IsUserInRoleEF("/", username, roleName);

   //using here solved the issue
   using (var en = result.GetEnumerator()) 
   {
     if (!en.MoveNext())
       throw new Exception("emty result of aspnet_UsersInRoles_IsUserInRoleEF");
     int? resultData = en.Current;

     return resultData == 1;//1 = success, see T-SQL for return codes
   }
}

1

เราเริ่มเห็นข้อผิดพลาดนี้"ไม่อนุญาตให้ทำธุรกรรมใหม่เนื่องจากมีเธรดอื่นทำงานอยู่ในเซสชัน"หลังจากย้ายจาก EF5 เป็น EF6

Google นำเรามาที่นี่ แต่เราไม่ได้โทร SaveChanges()เข้าไปในวง เกิดข้อผิดพลาดเมื่อเรียกใช้โพรซีเดอร์ที่เก็บไว้โดยใช้ ObjectContext.ExecuteFunction ภายในการวนลูป foreach จากฐานข้อมูล

การเรียกใช้ ObjectContext.ExecuteFunction ใด ๆ ล้อมรอบฟังก์ชันในธุรกรรม การเริ่มต้นธุรกรรมในขณะที่มีผู้อ่านที่เปิดอยู่ทำให้เกิดข้อผิดพลาด

เป็นไปได้ที่จะปิดการใช้งานการตัด SP ในการทำธุรกรรมโดยการตั้งค่าตัวเลือกต่อไปนี้

_context.Configuration.EnsureTransactionsForFunctionsAndCommands = false;

EnsureTransactionsForFunctionsAndCommandsตัวเลือกที่ช่วยให้ SP เพื่อให้ทำงานได้โดยไม่ต้องสร้างการทำธุรกรรมของตัวเองและข้อผิดพลาดที่ไม่ยก

DbContextConfiguration.EnsureTransactionsForFunctionsAndCommands คุณสมบัติ


0

ฉันไปงานปาร์ตี้ช้ามาก แต่วันนี้ฉันพบข้อผิดพลาดเดียวกันและวิธีการที่ฉันแก้ไขนั้นง่าย สถานการณ์ของฉันคล้ายกับรหัสที่ให้ฉันทำธุรกรรม DB ภายในซ้อนกันสำหรับแต่ละลูป

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

สำหรับสถานการณ์ที่กล่าวถึงข้างต้นการแก้ปัญหาจะเป็นดังนี้:

foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                {
private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
                    if (companyFeedDetail.FeedSourceTable.ToUpper() == "AUTO")
                    {
                        var company = (from a in _dbRiv.Company.Include("Product") where a.CompanyId == companyFeedDetail.CompanyId select a).First();
                        foreach (RivWorks.Model.NegotiationAutos.Auto sourceProduct in client.Auto)
                        {
                            foreach (RivWorks.Model.Negotiation.Product targetProduct in company.Product)
                            {
                                if (targetProduct.alternateProductID == sourceProduct.AutoID)
                                {
                                    found = true;
                                    break;
                                }
                            }
                            if (!found)
                            {
                                var newProduct = new RivWorks.Model.Negotiation.Product();
                                newProduct.alternateProductID = sourceProduct.AutoID;
                                newProduct.isFromFeed = true;
                                newProduct.isDeleted = false;
                                newProduct.SKU = sourceProduct.StockNumber;
                                company.Product.Add(newProduct);
                            }
                        }
                        _dbRiv.SaveChanges();  // ### THIS BREAKS ### //
                    }
                }

0

ฉันสายไปหน่อย แต่ฉันก็มีข้อผิดพลาดเช่นกัน ฉันแก้ไขปัญหาด้วยการตรวจสอบว่าค่าที่อัปเดตอยู่ที่ใด

ฉันพบว่าการสืบค้นของฉันผิดและมีการแก้ไขมากกว่า 250 รายการที่รอดำเนินการ ดังนั้นฉันจึงแก้ไขแบบสอบถามของฉันและตอนนี้ก็ทำงานได้ถูกต้อง

ดังนั้นในสถานการณ์ของฉัน:ตรวจสอบข้อผิดพลาดโดยการตรวจแก้จุดบกพร่องผลลัพธ์ที่แบบสอบถามส่งกลับ หลังจากนั้นแก้ไขแบบสอบถาม

หวังว่านี่จะช่วยแก้ไขปัญหาในอนาคต

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