Entity Framework Queryable async

98

Я работаю над некоторыми вещами веб-API, используя Entity Framework 6, и один из моих методов контроллера - это «Получить все», который ожидает получить содержимое таблицы из моей базы данных как IQueryable<Entity>. В моем репозитории мне интересно, есть ли какие-либо преимущества для того, чтобы делать это асинхронно, поскольку я новичок в использовании EF с async.

В основном это сводится к

 public async Task<IQueryable<URL>> GetAllUrlsAsync()
 {
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
 }

против

 public IQueryable<URL> GetAllUrls()
 {
    return context.Urls.AsQueryable();
 }

Будет ли асинхронная версия действительно давать здесь преимущества в производительности, или я несу ненужные накладные расходы, сначала проецируя на List (используя async, заметьте), а ЗАТЕМ перехожу к IQueryable?

Джесси Картер
источник
1
context.Urls имеет тип DbSet <URL>, который реализует IQueryable <URL>, поэтому .AsQueryable () является избыточным. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx Предполагая, что вы следовали шаблонам, которые предоставляет EF, или использовали инструменты, которые создают для вас контекст.
Sean B

Ответы:

225

Проблема, похоже, в том, что вы неправильно поняли, как async / await работают с Entity Framework.

О Entity Framework

Итак, посмотрим на этот код:

public IQueryable<URL> GetAllUrls()
{
    return context.Urls.AsQueryable();
}

и пример его использования:

repo.GetAllUrls().Where(u => <condition>).Take(10).ToList()

Что там происходит?

  1. Мы получаем IQueryableобъект (пока не получаем доступ к базе данных), используяrepo.GetAllUrls()
  2. Создаем новый IQueryableобъект с заданным условием, используя.Where(u => <condition>
  3. Мы создаем новый IQueryableобъект с указанным пределом разбиения на страницы, используя.Take(10)
  4. Мы получаем результаты из базы данных с помощью .ToList(). Наш IQueryableобъект скомпилирован в sql (вроде select top 10 * from Urls where <condition>). И база данных может использовать индексы, sql-сервер отправляет вам только 10 объектов из вашей базы данных (не все миллиарды URL-адресов, хранящиеся в базе данных)

Хорошо, посмотрим на первый код:

public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
}

На том же примере использования мы получили:

  1. Мы загружаем в память все миллиард URL-адресов, хранящихся в вашей базе данных, используя await context.Urls.ToListAsync();.
  2. У нас переполнение памяти. Правильный способ убить ваш сервер

О async / await

Почему предпочтительнее использовать async / await? Посмотрим на этот код:

var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));

Что здесь происходит?

  1. Начиная со строки 1 var stuff1 = ...
  2. Мы отправляем запрос на sql server, который хотим получить кое-что1 для userId
  3. Ждем (текущий поток заблокирован)
  4. Ждем (текущий поток заблокирован)
  5. .....
  6. Sql-сервер отправит нам ответ
  7. Переходим к строке 2 var stuff2 = ...
  8. Мы отправляем запрос на sql-сервер, который хотим получить кое-что2 для userId
  9. Ждем (текущий поток заблокирован)
  10. И опять
  11. .....
  12. Sql-сервер отправит нам ответ
  13. Оказываем вид

Итак, давайте посмотрим на его асинхронную версию:

var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));

Что здесь происходит?

  1. Отправляем запрос на сервер sql, чтобы получить файл stuff1 (строка 1)
  2. Отправляем запрос на sql server для получения stuff2 (строка 2)
  3. Ждем ответов от sql сервера, но текущий поток не заблокирован, он может обрабатывать запросы от других пользователей
  4. Оказываем вид

Правильный способ сделать это

Итак, хороший код здесь:

using System.Data.Entity;

public IQueryable<URL> GetAllUrls()
{
   return context.Urls.AsQueryable();
}

public async Task<List<URL>> GetAllUrlsByUser(int userId) {
   return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
}

Обратите внимание, что вы должны добавить using System.Data.Entity, чтобы использовать метод ToListAsync()для IQueryable.

Обратите внимание: если вам не нужны фильтрация, разбивка по страницам и прочее, вам не нужно работать IQueryable. Вы можете просто использовать await context.Urls.ToListAsync()и работать с материализованным List<Url>.

Виктор Лова
источник
3
@Korijn, глядя на картинку i2.iis.net/media/7188126/… из Введение в архитектуру IIS, могу сказать, что все запросы в IIS обрабатываются асинхронно
Виктор Лова
7
Поскольку вы не воздействуете на набор результатов в GetAllUrlsByUserметоде, вам не нужно делать его асинхронным. Просто верните Задачу и избавьте себя от ненужного конечного автомата от генерации компилятором.
Джонатон Саллинджер,
1
@JohnathonSullinger Хотя это будет работать в счастливом потоке, разве это не имеет побочного эффекта, что любое исключение не будет появляться здесь и распространяться на первое место, где есть ожидание? (Не то чтобы это обязательно плохо, но это изменение поведения?)
Генри Бин
9
Интересно, что никто не замечает, что второй пример кода в «Об async / await» абсолютно бессмысленен, потому что он вызовет исключение, поскольку ни EF, ни EF Core не являются потокобезопасными, поэтому попытка параллельного запуска просто вызовет исключение.
Tseng
1
Хотя это правильный ответ, я бы рекомендовал избегать использования asyncи, awaitесли вы ничего НЕ делаете со списком. Пусть звонящий к awaitнему. Ожидая вызова на этом этапе, return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();вы создаете дополнительную асинхронную оболочку, когда декомпилируете сборку и смотрите на IL.
Али Хакпури
10

В опубликованном вами примере первой версии есть огромная разница:

var urls = await context.Urls.ToListAsync();

Это плохо , в основном это так select * from table, возвращает все результаты в память, а затем применяет whereпротив них в сборе памяти, а не select * from table where...против базы данных.

Второй метод фактически не попадет в базу данных до тех пор, пока запрос не будет применен к IQueryable(возможно, через .Where().Select()операцию в стиле linq, которая вернет только значения db, которые соответствуют запросу.

Если ваши примеры были сопоставимы, asyncверсия обычно будет немного медленнее на запрос, так как в конечном автомате, который генерирует компилятор, больше накладных расходов, чтобы обеспечить asyncфункциональность.

Однако основное отличие (и преимущество) заключается в том, что asyncверсия позволяет выполнять больше одновременных запросов, поскольку не блокирует поток обработки, пока он ожидает завершения ввода-вывода (запрос базы данных, доступ к файлу, веб-запрос и т. Д.).

Тревор Пилли
источник
7
пока запрос не будет применен к IQueryable .... ни IQueryable.Where, ни IQueryable.Select принудительно выполнить запрос. Приоритет применяет предикат, а последний применяет проекцию. Он не выполняется до тех пор, пока не будет использован материализующий оператор, например ToList, ToArray, Single или First.
JJS
0

Короче говоря,
IQueryableон предназначен для того, чтобы отложить процесс RUN и сначала построить выражение вместе с другими IQueryableвыражениями, а затем интерпретировать и запускать выражение в целом.
Но ToList()метод (или несколько подобных ему методов) предназначен для мгновенного выполнения выражения «как есть».
Ваш первый метод ( GetAllUrlsAsync) будет запущен немедленно, потому что IQueryableза ним следует ToListAsync()метод. следовательно, он выполняется мгновенно (асинхронно) и возвращает кучу IEnumerables.
Между тем ваш второй метод ( GetAllUrls) не запустится. Вместо этого он возвращает выражение, и CALLER этого метода отвечает за выполнение выражения.

Рзассар
источник