Entity Framework: Z tym poleceniem jest już otwarty DataReader

285

Korzystam z Entity Framework i od czasu do czasu dostaję ten błąd.

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

Mimo że nie wykonuję żadnego ręcznego zarządzania połączeniami.

ten błąd zdarza się sporadycznie.

kod wywołujący błąd (skrócony dla ułatwienia odczytu):

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

za pomocą opcji Usuń wzór, aby za każdym razem otwierać nowe połączenie.

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

}

wciąż problematyczne

dlaczego EF nie używałby połączenia, jeśli jest już otwarte.

Sonic Soul
źródło
1
Zdaję sobie sprawę, że to pytanie jest starożytne, ale chciałbym wiedzieć, jakiego typu jesteś predicatei jakie historicPredicatezmienne. Odkryłem, że jeśli przejdziesz Func<T, bool>do Where()niego, to się skompiluje, a czasem zadziała (ponieważ robi to „gdzie” w pamięci). Co powinno się robić to przechodząc Expression<Func<T, bool>>do Where().
James

Odpowiedzi:

351

Nie chodzi o zamykanie połączenia. EF poprawnie zarządza połączeniem. Rozumiem ten problem, ponieważ istnieje wiele poleceń pobierania danych wykonanych przy jednym połączeniu (lub pojedynczym poleceniu z wieloma zaznaczeniami), podczas gdy następny czytnik danych jest wykonywany przed zakończeniem odczytu. Jedynym sposobem uniknięcia tego wyjątku jest umożliwienie wielu zagnieżdżonych DataReaders = włączenie MultipleActiveResultSets. Innym scenariuszem, w którym zawsze tak się dzieje, jest iteracja przez wynik zapytania (IQueryable) i wyzwalanie leniwego ładowania dla załadowanej jednostki wewnątrz iteracji.

Ladislav Mrnka
źródło
2
to miałoby sens. ale w każdej metodzie jest tylko jeden wybór.
Sonic Soul
1
@Sonic: To jest pytanie. Może wykonano więcej niż jedno polecenie, ale go nie widzisz. Nie jestem pewien, czy można to prześledzić w programie Profiler (wyjątek można zgłosić przed uruchomieniem drugiego czytnika). Możesz także spróbować wysłać zapytanie do ObjectQuery i wywołać ToTraceString, aby zobaczyć polecenie SQL. Trudno to wyśledzić. Zawsze włączam MARS.
Ladislav Mrnka
2
@Sonic: Nie miałem zamiaru sprawdzać wykonanych i zakończonych poleceń SQL.
Ladislav Mrnka
11
świetnie, moim problemem był drugi scenariusz: „gdy przejdziesz przez wynik zapytania (IQueryable) i uruchomisz leniwe ładowanie załadowanego obiektu wewnątrz iteracji”.
Amr Elgarhy,
6
Włączenie MARS może najwyraźniej mieć złe skutki uboczne: designlimbo.com/?p=235
Søren Boisen
126

Alternatywnie do korzystania z MARS (MultipleActiveResultSets) możesz napisać kod, aby nie otwierać wielu zestawów wyników.

Możesz odzyskać dane do pamięci, w ten sposób nie będziesz mieć otwartego czytnika. Często jest to spowodowane iteracją zestawu wyników podczas próby otwarcia innego zestawu wyników.

Przykładowy kod:

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

Powiedzmy, że przeprowadzasz wyszukiwanie w bazie danych zawierającej:

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

Możemy to zrobić w prosty sposób, dodając .ToList () w następujący sposób:

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

To zmusza encjifrfrowej do załadowania listy do pamięci, dlatego gdy iterujemy ją w pętli foreach, nie korzysta już z czytnika danych do otwierania listy, zamiast tego jest w pamięci.

Zdaję sobie sprawę, że może to nie być pożądane, jeśli chcesz na przykład załadować niektóre właściwości. Jest to głównie przykład, który, mam nadzieję, wyjaśnia, w jaki sposób / dlaczego możesz dostać ten problem, abyś mógł odpowiednio podejmować decyzje

Jim Wolff
źródło
7
To rozwiązanie działało dla mnie. Dodaj .ToList () zaraz po zapytaniu i przed wykonaniem czegokolwiek innego z wynikiem.
TJKjaer
9
Uważaj na to i kieruj się zdrowym rozsądkiem. Jeśli ToListwymyślisz tysiąc obiektów, zwiększy to pamięć o tonę. W tym konkretnym przykładzie lepiej byłoby połączyć zapytanie wewnętrzne z pierwszym, aby wygenerować tylko jedno zapytanie, a nie dwa.
kamranicus
4
@subkamran Miałem na myśli dokładnie to, myśląc o czymś i wybierając to, co jest odpowiednie dla danej sytuacji, a nie tylko robiąc. Przykład jest po prostu czymś losowym, co wymyśliłem, aby wyjaśnić :)
Jim Wolff
3
Zdecydowanie chciałem tylko wyraźnie to wskazać dla ludzi
chętnych
Nie strzelaj do mnie, ale to w żaden sposób nie rozwiązuje problemu. Od kiedy „pobieranie danych do pamięci” jest rozwiązaniem problemu związanego z SQL? Lubię rozmawiać z bazą danych, więc w żaden sposób nie wolałbym wyciągać czegoś z pamięci „ponieważ w przeciwnym razie zostanie zgłoszony wyjątek SQL”. Niemniej jednak w dostarczonym kodzie nie ma powodu, aby kontaktować się z bazą danych dwukrotnie. Łatwo to zrobić za jednym razem. Uważaj na posty tego typu. ToList, First, Single, ... Powinno być używane tylko wtedy, gdy dane są potrzebne w pamięci (a więc tylko te, które CHCESZ), a nie gdy wyjątek SQL występuje inaczej.
Frederik Prijck
70

Jest inny sposób na rozwiązanie tego problemu. To, czy jest to lepszy sposób, zależy od twojej sytuacji.

Problem wynika z leniwego ładowania, więc jednym ze sposobów uniknięcia tego jest brak leniwego ładowania, poprzez użycie opcji Dołącz:

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

Jeśli użyjesz odpowiednich Includeliter, możesz uniknąć włączenia MARS. Ale jeśli go przegapisz, pojawi się błąd, więc włączenie MARS jest prawdopodobnie najłatwiejszym sposobem na jego naprawienie.

Ryan Lundy
źródło
1
Działa jak urok. .Includejest znacznie lepszym rozwiązaniem niż włączenie MARS i znacznie łatwiejszym niż pisanie własnego kodu zapytania SQL.
Nolonar
15
Jeśli ktoś ma problem polegający na tym, że możesz napisać tylko .Include („ciąg”), a nie lambda, musisz dodać „using System.Data.Entity”, ponieważ tam znajduje się metoda rozszerzenia.
Jim Wolff,
46

Ten błąd pojawia się, gdy kolekcja, którą próbujesz iterować, jest swego rodzaju leniwym ładowaniem (IQueriable).

foreach (var user in _dbContext.Users)
{    
}

Przekształcenie kolekcji IQueriable w inną kolekcję wymienną rozwiąże ten problem. przykład

_dbContext.Users.ToList()

Uwaga: .ToList () tworzy nowy zestaw za każdym razem i może powodować problemy z wydajnością w przypadku dużych danych.

Nalan Madheswaran
źródło
1
Najłatwiejsze możliwe rozwiązanie! Big UP;)
Jacob Sobus
1
Pobieranie niepowiązanych list może powodować poważne problemy z wydajnością! Jak ktokolwiek może to głosować?
SandRock,
1
@ SandRock nie dla kogoś, kto pracuje dla małej firmy - SELECT COUNT(*) FROM Users= 5
Simon_Weaver 29.04.17
5
Zastanów się dwa razy. Młody programista czytający to pytanie może pomyśleć, że jest to wszechstronne rozwiązanie, gdy absolutnie tak nie jest. Sugeruję edycję odpowiedzi, aby ostrzec czytelników przed niebezpieczeństwem pobierania niepowiązanych list z bazy danych.
SandRock
1
@ SandRock Myślę, że byłoby to dobre miejsce na link do odpowiedzi lub artykułu opisującego najlepsze praktyki.
Sinjai,
13

Rozwiązałem problem łatwo (pragmatycznie), dodając opcję do konstruktora. Dlatego używam tego tylko w razie potrzeby.

public class Something : DbContext
{
    public Something(bool MultipleActiveResultSets = false)
    {
        this.Database
            .Connection
            .ConnectionString = Shared.ConnectionString /* your connection string */
                              + (MultipleActiveResultSets ? ";MultipleActiveResultSets=true;" : "");
    }
...
Harvey Triana
źródło
2
Dziękuję Ci. To działa. Właśnie dodałem MultipleActiveResultSets = true w ciągu połączenia bezpośrednio w web.config
Mosharaf Hossain
11

Spróbuj ustawić parametry połączenia MultipleActiveResultSets=true. Umożliwia to wielozadaniowość w bazie danych.

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

To działa dla mnie ... niezależnie od tego, czy połączenie w app.config, czy też ustawiasz je programowo ... mam nadzieję, że będzie to pomocne

Mohamed Hocine
źródło
MultipleActiveResultSets = true dodane do ciągu połączenia prawdopodobnie rozwiąże problem. To nie powinno było zostać odrzucone.
Aaron Hudon
tak, na pewno pokazałem, jak dodać do ciągu połączenia
Mohamed Hocine,
4

Pierwotnie zdecydowałem się użyć pola statycznego w mojej klasie API, aby odwołać się do instancji obiektu MyDataContext (gdzie MyDataContext jest obiektem kontekstu EF5), ale to właśnie wydawało się stwarzać problem. Dodałem kod podobny do następującego do każdej z moich metod API i to rozwiązało problem.

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

Jak zauważyli inni, obiekty kontekstu danych EF NIE są bezpieczne dla wątków. Umieszczenie ich w obiekcie statycznym ostatecznie spowoduje błąd „czytnika danych” w odpowiednich warunkach.

Moje pierwotne założenie było takie, że utworzenie tylko jednej instancji obiektu byłoby bardziej wydajne i zapewniłoby lepsze zarządzanie pamięcią. Z tego, co zebrałem, badając ten problem, tak nie jest. W rzeczywistości wydaje się, że bardziej efektywne jest traktowanie każdego wywołania interfejsu API jako izolowanego, bezpiecznego dla wątków zdarzenia. Zapewnienie prawidłowego zwolnienia wszystkich zasobów, gdy obiekt wykracza poza zakres.

Ma to sens zwłaszcza, jeśli przejdziesz do następnego naturalnego postępu API, który polegałby na ujawnieniu go jako WebService lub REST API.

Ujawnienie

  • System operacyjny: Windows Server 2012
  • .NET: Zainstalowano 4.5, Projektuj przy użyciu 4.0
  • Źródło danych: MySQL
  • Struktura aplikacji: MVC3
  • Uwierzytelnianie: formularze
Jeffrey A. Gochin
źródło
3

Zauważyłem, że ten błąd występuje, gdy wysyłam IQueriable do widoku i używam go w podwójnym foreach, gdzie wewnętrzny foreach również musi korzystać z połączenia. Prosty przykład (ViewBag.parents może być IQueriable lub DbSet):

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

    }
}

Najprostszym rozwiązaniem jest użycie .ToList()tej kolekcji przed użyciem. Zauważ też, że MARS nie działa z MySQL.

cen
źródło
DZIĘKUJĘ CI! Wszystko tutaj mówiło „problem stanowi zagnieżdżone pętle”, ale nikt nie powiedział, jak to naprawić. Włączyłem ToList()swoje pierwsze połączenie, aby uzyskać kolekcję z bazy danych. Potem zrobiłem a foreachna tej liście, a kolejne wywołania działały idealnie zamiast dawać błąd.
AlbatrossCafe
@AlbatrossCafe ... ale nikt nie wspomina, że ​​w takim przypadku dane zostaną załadowane do pamięci, a zapytanie zostanie wykonane w pamięci zamiast DB
Lightning3
3

Odkryłem, że mam ten sam błąd i wystąpił, gdy użyłem Func<TEntity, bool>zamiast zamiast Expression<Func<TEntity, bool>>dla twojego predicate.

Raz zmieniło się wszystko Func's, aby Expression'swyjątkiem przestał wyrzucane.

Wierzę, że EntityFramworkrobi to pewne sprytne rzeczy, z Expression'sktórymi po prostu nie ma nic wspólnegoFunc's

sQuir3l
źródło
To wymaga więcej pozytywnych opinii. Próbowałem stworzyć metodę w mojej klasie DataContext, przyjmując (MyTParent model, Func<MyTChildren, bool> func)tak, aby moje ViewModels mogły określić określoną whereklauzulę dla ogólnej metody DataContext. Nic nie działało, dopóki tego nie zrobiłem.
Justin
3

2 rozwiązania mające na celu złagodzenie tego problemu:

  1. Wymuś buforowanie pamięci, utrzymując leniwe ładowanie .ToList() po zapytaniu, dzięki czemu możesz iterować przez niego otwierając nowy czytnik danych.
  2. .Include(/ dodatkowe encje, które chcesz załadować w zapytaniu /) nazywa się to chętnym ładowaniem, co pozwala (rzeczywiście) dołączyć powiązane obiekty (encje) podczas wykonywania zapytania za pomocą DataReadera.
Stefano Beltrame
źródło
2

Dobrym pośrednikiem między włączeniem MARS a pobraniem całego zestawu wyników do pamięci jest pobranie tylko identyfikatorów w początkowym zapytaniu, a następnie przechodzenie przez identyfikatory materializujące każdą jednostkę w trakcie podróży.

Na przykład (używając przykładowych encji „Blog i posty” jak w tej odpowiedzi ):

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

Oznacza to, że wciąga się do pamięci tylko kilka tysięcy liczb całkowitych, w przeciwieństwie do tysięcy całych wykresów obiektowych, co powinno zminimalizować zużycie pamięci, umożliwiając jednocześnie pracę element po elemencie bez włączania MARS.

Inną zaletą tego, jak widać w przykładzie, jest to, że można zapisywać zmiany podczas przechodzenia przez każdy element, zamiast czekać na koniec pętli (lub inne tego rodzaju obejście), co byłoby potrzebne nawet przy MARS włączony (patrz tutaj i tutaj ).

Paweł
źródło
context.SaveChanges();inside loop :(. To nie jest dobre. musi być poza pętlą.
Jawand Singh
1

W moim przypadku stwierdziłem, że przed wywołaniami myContext.SaveChangesAsync () brakowało instrukcji „czekaj”. Dodanie opcji oczekiwania przed tymi wywołaniami asynchronicznymi naprawiło dla mnie problemy z czytnikiem danych.

Elijah Lofgren
źródło
0

Jeśli spróbujemy zgrupować część naszych warunków w Func <> lub metodzie rozszerzenia, otrzymamy ten błąd, załóżmy, że mamy taki kod:

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) { .... }

Spowoduje to wyrzucenie wyjątku, jeśli spróbujemy użyć go w Where (), zamiast tego powinniśmy zbudować Predicate:

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

Więcej informacji można znaleźć na stronie : http://www.albahari.com/nutshell/predicatebuilder.aspx

Arvand
źródło
0

Problem ten można rozwiązać, po prostu przekształcając dane w listę

 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;
            }
Debendra Dash
źródło
ToList () wykonuje wywołanie, ale powyższy kod nadal nie usuwa połączenia. więc twój tekst internetowy wciąż jest zagrożony zamknięciem w czasie linii 1
Sonic Soul
0

W mojej sytuacji problem wystąpił z powodu rejestracji wstrzyknięcia zależności. Wstrzykiwałem usługę zakresu na żądanie, która używała dbcontext do zarejestrowanej usługi singleton. W związku z tym kontekst db db został użyty w ramach wielu żądań i stąd błąd.

E. Staal
źródło
0

W moim przypadku problem nie miał nic wspólnego z ciągiem połączenia MARS, ale z serializacją json. Po uaktualnieniu mojego projektu z NetCore2 do 3 dostałem ten błąd.

Więcej informacji można znaleźć tutaj

Albo
źródło
-6

Rozwiązałem ten problem, używając następującej sekcji kodu przed drugim zapytaniem:

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

możesz zmienić czas snu w milisekundach

PD Przydatne podczas używania wątków

i31nGo
źródło
13
Dowolne dodawanie Thread.Sleep w dowolnym rozwiązaniu jest złą praktyką - i jest szczególnie złe, gdy stosuje się go w celu uniknięcia innego problemu, w którym stan pewnej wartości nie jest w pełni zrozumiany. Myślałem, że „Używanie wątków”, jak podano na dole odpowiedzi, oznaczałoby przynajmniej pewne podstawowe zrozumienie wątków - ale ta odpowiedź nie uwzględnia żadnego kontekstu, szczególnie te okoliczności, w których jest to bardzo zły pomysł używać Thread.Sleep - na przykład w wątku interfejsu użytkownika.
Mike Tours,