Entity Framework: вже є відкритий DataReader, пов'язаний з цією командою


285

Я використовую Entity Framework і час від часу я отримуватиму цю помилку.

EntityCommandExecutionException
{"There is already an open DataReader associated with this Command which must be closed first."}
   at System.Data.EntityClient.EntityCommandDefinition.ExecuteStoreCommands...

Навіть не дивлячись на те, що я не займаюся ручним управлінням з'єднаннями.

ця помилка трапляється з перервами.

код, який запускає помилку (скорочується для зручності читання):

        if (critera.FromDate > x) {
            t= _tEntitites.T.Where(predicate).ToList();
        }
        else {
            t= new List<T>(_tEntitites.TA.Where(historicPredicate).ToList());
        }

використовуючи шаблон розпорядження, щоб кожен раз відкривати нове з'єднання.

using (_tEntitites = new TEntities(GetEntityConnection())) {

    if (critera.FromDate > x) {
        t= _tEntitites.T.Where(predicate).ToList();
    }
    else {
        t= new List<T>(_tEntitites.TA.Where(historicPredicate).ToList());
    }

}

все ще проблематично

чому EF не використовуватиме повторно з'єднання, якщо воно вже відкрите.


1
Я усвідомлюю, що це питання давнє, але мені було б цікаво дізнатись про тип predicateі historicPredicateзмінні. Я виявив, що якщо ви перейдете Func<T, bool>до Where()нього, він буде компілювати, а іноді працювати (тому що це робить "де" в пам'яті). Те, що ви повинні робити, переходить Expression<Func<T, bool>>до цього Where().
Джеймс

Відповіді:


351

Йдеться не про закриття з'єднання. EF правильно керує з'єднанням. Моє розуміння цієї проблеми полягає в тому, що існує кілька команд пошуку даних, які виконуються на одному з'єднанні (або одна команда з декількома виборами), тоді як наступний DataReader виконується до того, як перше завершило читання. Єдиний спосіб уникнути винятку - дозволити кілька вкладених DataReaders = увімкнути MultipleActiveResultSets. Інший сценарій, коли це завжди відбувається, це коли ви повторюєте результат запиту (IQueryable), і ви запускаєте ледаче завантаження для завантаженого об'єкта всередині ітерації.


2
це мало б сенс. але в кожному методі є лише один вибір.
Sonic Soul

1
@Sonic: Це питання. Можливо, більше однієї команди виконано, але ви її не бачите. Я не впевнений, чи це можна простежити в Profiler (виняток можна кинути перед виконанням другого зчитувача). Ви також можете спробувати передати запит ObjectQuery і зателефонувати на ToTraceString, щоб побачити команду SQL. Це важко відстежити. Я завжди включаю MARS.
Ladislav Mrnka

2
@Sonic: Я не мав наміру перевіряти виконані та виконані команди SQL.
Ladislav Mrnka

11
чудово, моя проблема полягала в другому сценарії: "коли ви повторите результат запиту (IQueryable), і ви запустить ледачу завантаження завантаженого об'єкта всередині ітерації."
Amr Elgarhy

6
Увімкнення MARS, очевидно, може мати погані побічні ефекти: designlimbo.com/?p=235
Søren Boisen

126

Як варіант використання MARS (MultipleActiveResultSets), ви можете написати свій код, щоб не відкривати кілька наборів результатів.

Що ви можете зробити - це відновити дані в пам'ять, і таким чином у вас не буде відкритого читача. Часто це викликано ітерацією через набір результатів при спробі відкрити інший набір результатів.

Приклад коду:

public class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

public class Blog
{
    public int BlogID { get; set; }
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int PostID { get; set; }
    public virtual Blog Blog { get; set; }
    public string Text { get; set; }
}

Скажімо, ви робите пошук у вашій базі даних, що містить:

var context = new MyContext();

//here we have one resultset
var largeBlogs = context.Blogs.Where(b => b.Posts.Count > 5); 

foreach (var blog in largeBlogs) //we use the result set here
{
     //here we try to get another result set while we are still reading the above set.
    var postsWithImportantText = blog.Posts.Where(p=>p.Text.Contains("Important Text"));
}

Ми можемо зробити просте рішення для цього, додавши .ToList (), як це:

var largeBlogs = context.Blogs.Where(b => b.Posts.Count > 5).ToList();

Це змушує підрозділ кадрів завантажувати список у пам'ять, таким чином, коли ми повторюємо, хоча він у циклі foreach вже не використовує зчитувач даних для відкриття списку, він замість цього знаходиться в пам'яті.

Я усвідомлюю, що це може бути не бажаним, якщо ви хочете, наприклад, ліниво завантажити деякі властивості. Це здебільшого приклад, який, сподіваємось, пояснює, як / чому у вас може виникнути ця проблема, щоб ви могли приймати рішення відповідно


7
Це рішення спрацювало на мене. Додайте .ToList () відразу після запиту та перед тим, як робити щось інше з результатом.
TJKjaer

9
Будьте обережні з цим і використовуйте здоровий глузд. Якщо ви ToListвикористовуєте тисячу об'єктів, це збільшить пам'ять на тонну. У цьому конкретному прикладі вам краще поєднувати внутрішній запит із першим, тому генерується лише один запит, а не два.
kamranicus

4
@subkamran Моя думка полягала саме в тому, що думати про щось і вибирати, що підходить для ситуації, а не просто робити. Приклад - це щось щось випадкове, що я придумав пояснити :)
Джим Волф

3
Безумовно, я просто хотів вказати це прямо для людей, які копіюють / вставляють :)
kamranicus

Не стріляйте в мене, але це жодним чином не вирішує питання. З тих пір, коли "витягування даних у пам'ять" є рішенням проблеми, пов’язаної з SQL? Мені подобається базікати з базою даних, тому я ні в якому разі не вважаю за краще щось витягувати в пам'яті, "бо в іншому випадку викидається виняток SQL". Тим не менш, у наданому вами коді немає підстав звертатися до бази даних двічі. Це легко зробити за один дзвінок. Будьте обережні з подібними повідомленнями. ToList, First, Single, ... Використовується лише тоді, коли дані потрібні в пам'яті (тому лише ті дані, ЩО ХОЧУТЬ), а не тоді, коли виняток SQL виникає в іншому випадку.
Фредерік Прийк

70

Є ще один спосіб подолати цю проблему. Чи буде це кращий спосіб, залежить від вашої ситуації.

Проблема виникає внаслідок ледачого завантаження, тому один із способів уникнути цього - не мати ледачого завантаження, використовуючи Включити:

var results = myContext.Customers
    .Include(x => x.Orders)
    .Include(x => x.Addresses)
    .Include(x => x.PaymentMethods);

Якщо ви використовуєте відповідні Includes, ви можете уникнути включення MARS. Але якщо ви пропустите її, ви отримаєте помилку, тому включення MARS, мабуть, найпростіший спосіб виправити.


1
Працював як шарм. .Includeє набагато кращим рішенням, ніж включення MARS, і набагато простіше, ніж написання власного коду запиту SQL.
Нолонар

15
Якщо у когось виникає проблема, що ви можете написати лише .Include ("рядок") не лямбда, вам потрібно додати "за допомогою System.Data.Entity", оскільки там розташований метод розширення.
Джим Волф

46

Ви отримуєте цю помилку, коли колекція, яку ви намагаєтесь повторити, є ледачим завантаженням (IQueriable).

foreach (var user in _dbContext.Users)
{    
}

Перетворення колекції IQueriable в іншу численну колекцію вирішить цю проблему. приклад

_dbContext.Users.ToList()

Примітка. .ToList () створює новий набір щоразу, і це може спричинити проблеми з продуктивністю, якщо ви маєте справу з великими даними.


1
Найпростіше можливе рішення! Big UP;)
Яків Собус

1
Отримання необмежених списків може спричинити серйозні проблеми з продуктивністю! Як хто може виступити проти цього?
SandRock

1
@SandRock не для того, хто працює в невеликій компанії - SELECT COUNT(*) FROM Users= 5
Simon_Weaver

5
Подумайте двічі про це. Молодий розробник, який читає цю запитання, може подумати, що це рішення постійно, коли це абсолютно не так. Я пропоную вам відредагувати свою відповідь, щоб попередити читачів про небезпеку отримання необмежених списків з db.
SandRock

1
@SandRock Я думаю, що це було б хорошим місцем для посилання на відповідь чи статтю, що описують найкращі практики.
Sinjai

13

Я вирішив проблему легко (прагматично), додавши в конструктор варіант. Таким чином, я використовую це лише за потреби.

public class Something : DbContext
{
    public Something(bool MultipleActiveResultSets = false)
    {
        this.Database
            .Connection
            .ConnectionString = Shared.ConnectionString /* your connection string */
                              + (MultipleActiveResultSets ? ";MultipleActiveResultSets=true;" : "");
    }
...

2
Дякую. Це працює. Я щойно додав MultipleActiveResultSets = вірно в рядок з'єднання безпосередньо в web.config
Mosharaf Hossain

11

Спробуйте в своєму рядку з'єднання встановити MultipleActiveResultSets=true. Це дозволяє багатозадачність на базі даних.

Server=yourserver ;AttachDbFilename=database;User Id=sa;Password=blah ;MultipleActiveResultSets=true;App=EntityFramework

Це працює для мене ... будь то ваш зв’язок у app.config чи ви встановили це програмно ... сподіваюся, це корисне


MultipleActiveResultSets = true, доданий до рядка підключення, ймовірно, вирішить проблему. Це не повинно було бути запереченим.
Аарон Гудон

так, я впевнений, що я продемонстрував, як додати до своєї
лінії

4

Спочатку я вирішив використовувати статичне поле в своєму класі API для посилання на екземпляр об’єкта MyDataContext (де MyDataContext є об'єктом контексту EF5), але саме це, здавалося, створило проблему. Я додав код на зразок наступного до кожного мого методу API, і це вирішило проблему.

using(MyDBContext db = new MyDBContext())
{
    //Do some linq queries
}

Як заявили інші люди, об'єкти контексту даних EF НЕ безпечні для потоків. Тож розміщення їх у статичному об’єкті врешті призведе до помилки «зчитувача даних» за правильних умов.

Моє первісне припущення полягало в тому, що створення лише одного примірника об'єкта було б ефективнішим і дозволяло б краще керувати пам'яттю. З того, що я зібрав, досліджуючи це питання, це не так. Насправді, здається, більш ефективно ставитись до кожного дзвінка до вашого API як до події, що є безпечною для потоків. Забезпечення належного звільнення всіх ресурсів, оскільки об’єкт виходить із сфери застосування.

Це має сенс особливо, якщо ви перейдете на свій API до наступного природного прогресу, який би піддавав його як WebService або REST API.

Розкриття інформації

  • ОС: Windows Server 2012
  • .NET: Встановлено 4.5, проект за допомогою 4.0
  • Джерело даних: MySQL
  • Застосування програми: MVC3
  • Автентифікація: форми

3

Я помітив, що ця помилка трапляється, коли я надсилаю IQueriable до представлення даних і використовую його в подвійному передбаченні, де внутрішній foreach також повинен використовувати з'єднання. Простий приклад (ViewBag.parents може бути IQueriable або DbSet):

foreach (var parent in ViewBag.parents)
{
    foreach (var child in parent.childs)
    {

    }
}

Просте рішення - використовувати .ToList()в колекції перед її використанням. Також зауважте, що MARS не працює з MySQL.


СПАСИБІ! Тут все сказано "проблема вкладених циклів", але ніхто не сказав, як це виправити. Я ставлю ToList()свій перший дзвінок, щоб отримати колекцію з БД. Тоді я зробив foreachсписок у цьому списку, і наступні дзвінки працювали чудово, замість того, щоб видавати помилку.
AlbatrossCafe

@AlbatrossCafe ... але ніхто не згадує, що в такому випадку ваші дані будуть завантажені в пам'ять, а запит буде виконано в пам'яті замість БД
Lightning3

3

Я виявив, що у мене така ж помилка, і вона сталася, коли я використовував Func<TEntity, bool>замість Expression<Func<TEntity, bool>>вашого для вашого predicate.

Як тільки я змінив усіх, Func'sщоб Expression'sвиняток перестав кидати.

Я вважаю, що EntityFramworkце робить якісь розумні речі, з Expression'sякими він просто не робитьFunc's


Для цього потрібно більше коштів. Я намагався створити метод у своєму класі DataContext, приймаючи (MyTParent model, Func<MyTChildren, bool> func)так, щоб мої ViewModels могли вказати певний whereпункт до методу Generic DataContext. Нічого не працювало, поки я цього не зробив.
Джастін

3

2 рішення для усунення цієї проблеми:

  1. Примушуйте кешування пам’яті зберігати ледачий завантаження .ToList()після запиту, тож ви зможете повторити його, відкривши новий DataReader.
  2. .Include(/ додаткові об'єкти, які ви хочете завантажити в запиті /) це називається нетерплячим завантаженням, яке дозволяє (дійсно) включати пов'язані об'єкти (сутності) під час виконання запиту з DataReader.

2

Хороший середина між включенням MARS та завантаженням всього набору результатів у пам'ять - це отримати лише ідентифікатори в початковому запиті, а потім проглянути цикл ідентифікаторів, що матеріалізують кожну сутність, коли ви йдете.

Наприклад (використовуючи зразки "Блог і пости", як у цій відповіді ):

using (var context = new BlogContext())
{
    // Get the IDs of all the items to loop through. This is
    // materialized so that the data reader is closed by the
    // time we're looping through the list.
    var blogIds = context.Blogs.Select(blog => blog.Id).ToList();

    // This query represents all our items in their full glory,
    // but, items are only materialized one at a time as we
    // loop through them.
    var blogs =
        blogIds.Select(id => context.Blogs.First(blog => blog.Id == id));

    foreach (var blog in blogs)
    {
        this.DoSomethingWith(blog.Posts);

        context.SaveChanges();
    }
}

Це означає, що ви втягуєте в пам'ять лише кілька тисяч цілих чисел, на відміну від тисяч цілих графіків об'єктів, які повинні мінімізувати використання пам'яті, дозволяючи вам працювати по окремих пунктах без включення MARS.

Ще одна приємна перевага цього, як видно з зразка, полягає в тому, що ви можете зберігати зміни під час перегляду циклу кожного елемента, замість того, щоб чекати до кінця циклу (або якогось іншого такого способу), як це було б потрібно навіть для MARS увімкнено (див. Тут і тут ).


context.SaveChanges();всередині циклу :(. Це не добре. воно повинно бути поза петлею
Jawand Singh

1

У моєму випадку я виявив, що перед викликами myContext.SaveChangesAsync () були відсутні "заяви очікування". Додавання очікування перед тими викликами асинхронізації вирішило для мене проблеми з читанням даних.


0

Якщо ми спробуємо згрупувати частину наших умов у метод Func <> або розширення, ми отримаємо цю помилку, припустимо, у нас такий код:

public static Func<PriceList, bool> IsCurrent()
{
  return p => (p.ValidFrom == null || p.ValidFrom <= DateTime.Now) &&
              (p.ValidTo == null || p.ValidTo >= DateTime.Now);
}

Or

public static IEnumerable<PriceList> IsCurrent(this IEnumerable<PriceList> prices) { .... }

Це викине виняток, якщо ми спробуємо використовувати його у Where (), що замість цього ми повинні зробити, це створити предикат так:

public static Expression<Func<PriceList, bool>> IsCurrent()
{
    return p => (p.ValidFrom == null || p.ValidFrom <= DateTime.Now) &&
                (p.ValidTo == null || p.ValidTo >= DateTime.Now);
}

Детальніше можна прочитати на веб-сайті : http://www.albahari.com/nutshell/predicatebuilder.aspx


0

Цю проблему можна вирішити просто шляхом перетворення даних у список

 var details = _webcontext.products.ToList();


            if (details != null)
            {
                Parallel.ForEach(details, x =>
                {
                    Products obj = new Products();
                    obj.slno = x.slno;
                    obj.ProductName = x.ProductName;
                    obj.Price = Convert.ToInt32(x.Price);
                    li.Add(obj);

                });
                return li;
            }

ToList () здійснює дзвінок, але вищевказаний код все ще не розпоряджається з'єднанням. тож ваш _webcontext все ще ризикує бути закритим під час першого рядка
Sonic Soul

0

У моїй ситуації проблема виникла через реєстрацію ін'єкційної залежності. Я вводив службу на замовлення на запит, яка використовувала dbcontext, в послугу, зареєстровану одиночку. Для цього dbcontext використовувався в межах декількох запитів, а отже, і помилка.


0

У моєму випадку проблема не мала нічого спільного з рядком з'єднання MARS, а з серіалізацією json. Після оновлення мого проекту з NetCore2 до 3 я отримав цю помилку.

Більше інформації можна знайти тут


-6

Цю проблему я вирішив за допомогою наступного розділу коду перед другим запитом:

 ...first query
 while (_dbContext.Connection.State != System.Data.ConnectionState.Closed)
 {
     System.Threading.Thread.Sleep(500);
 }
 ...second query

ви можете змінити час сну в мілісекундах

PD Корисний при використанні ниток


13
Довільно додавати Thread.Sleep у будь-якому рішенні є поганою практикою - і особливо погано, коли використовується для того, щоб пройти іншу проблему, коли стан якоїсь цінності не зовсім зрозумілий. Я б міг подумати, що "Використання ниток", як сказано внизу відповіді, означало б принаймні деяке базове розуміння нитки - але ця відповідь не враховує жодного контексту, особливо тих обставин, коли це дуже погана ідея використовувати Thread.Sleep - наприклад, на потоці інтерфейсу користувача.
Майк Тур
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.