Entity Framework truy vấn không đồng bộ


96

Tôi đang làm việc trên một số nội dung API Web bằng cách sử dụng Entity Framework 6 và một trong những phương pháp trình điều khiển của tôi là "Nhận tất cả" mong đợi nhận nội dung của bảng từ cơ sở dữ liệu của tôi dưới dạng IQueryable<Entity>. Trong kho lưu trữ của mình, tôi đang tự hỏi liệu có lý do thuận lợi nào để thực hiện điều này không đồng bộ vì tôi mới sử dụng EF với không đồng bộ hay không.

Về cơ bản, nó tổng hợp thành

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

vs

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

Liệu phiên bản không đồng bộ có thực sự mang lại lợi ích về hiệu suất ở đây hay tôi phải gánh chịu chi phí không cần thiết bằng cách chiếu vào Danh sách trước (sử dụng tính năng không đồng bộ của bạn) và SAU đó sẽ chuyển đến IQueryable?


1
context.Urls thuộc kiểu DbSet <URL> thực thi IQueryable <URL> nên .AsQueryable () là dư thừa. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx Giả sử bạn đã làm theo các mẫu mà EF cung cấp hoặc sử dụng công cụ tạo ngữ cảnh cho bạn.
Sean B

Câu trả lời:


222

Có vẻ như vấn đề là bạn đã hiểu sai cách async / await hoạt động với Entity Framework.

Giới thiệu về Khung thực thể

Vì vậy, hãy xem mã này:

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

và ví dụ về cách sử dụng nó:

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

Điều gì xảy ra ở đó?

  1. Chúng tôi đang nhận được IQueryableđối tượng (chưa truy cập cơ sở dữ liệu) bằng cách sử dụngrepo.GetAllUrls()
  2. Chúng tôi tạo một IQueryableđối tượng mới với điều kiện được chỉ định bằng cách sử dụng.Where(u => <condition>
  3. Chúng tôi tạo một IQueryableđối tượng mới với giới hạn phân trang được chỉ định bằng cách sử dụng.Take(10)
  4. Chúng tôi lấy kết quả từ cơ sở dữ liệu bằng cách sử dụng .ToList(). IQueryableĐối tượng của chúng ta được biên dịch thành sql (like select top 10 * from Urls where <condition>). Và cơ sở dữ liệu có thể sử dụng các chỉ mục, máy chủ sql chỉ gửi cho bạn 10 đối tượng từ cơ sở dữ liệu của bạn (không phải tất cả tỷ url được lưu trữ trong cơ sở dữ liệu)

Được rồi, hãy xem mã đầu tiên:

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

Với cùng một ví dụ về cách sử dụng, chúng tôi nhận được:

  1. Chúng tôi đang tải trong bộ nhớ tất cả tỷ url được lưu trữ trong cơ sở dữ liệu của bạn bằng cách sử dụng await context.Urls.ToListAsync();.
  2. Chúng tôi bị tràn bộ nhớ. Cách đúng để giết máy chủ của bạn

Giới thiệu về async / await

Tại sao async / await được ưu tiên sử dụng? Hãy xem mã này:

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

chuyện gì xảy ra ở đây thế?

  1. Bắt đầu từ dòng 1 var stuff1 = ...
  2. Chúng tôi gửi yêu cầu đến máy chủ sql mà chúng tôi muốn nhận một số nội dung1 cho userId
  3. Chúng tôi đợi (chuỗi hiện tại bị chặn)
  4. Chúng tôi đợi (chuỗi hiện tại bị chặn)
  5. .....
  6. Máy chủ Sql gửi phản hồi cho chúng tôi
  7. Chúng tôi chuyển sang dòng 2 var stuff2 = ...
  8. Chúng tôi gửi yêu cầu đến máy chủ sql mà chúng tôi muốn nhận một số thứ2 cho userId
  9. Chúng tôi đợi (chuỗi hiện tại bị chặn)
  10. Và một lần nữa
  11. .....
  12. Máy chủ Sql gửi phản hồi cho chúng tôi
  13. Chúng tôi kết xuất chế độ xem

Vì vậy, hãy xem xét một phiên bản không đồng bộ của nó:

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

chuyện gì xảy ra ở đây thế?

  1. Chúng tôi gửi yêu cầu đến máy chủ sql để lấy thứ1 (dòng 1)
  2. Chúng tôi gửi yêu cầu đến máy chủ sql để lấy thứ 2 (dòng 2)
  3. Chúng tôi chờ phản hồi từ máy chủ sql, nhưng luồng hiện tại không bị chặn, anh ấy có thể xử lý các truy vấn từ người dùng khác
  4. Chúng tôi kết xuất chế độ xem

Đúng cách để làm điều đó

Vì vậy, mã tốt ở đây:

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

Lưu ý, hơn bạn phải thêm using System.Data.Entityđể sử dụng phương thức ToListAsync()cho IQueryable.

Lưu ý rằng nếu bạn không cần lọc và phân trang và nội dung, bạn không cần phải làm việc với IQueryable. Bạn chỉ có thể sử dụng await context.Urls.ToListAsync()và làm việc với materialized List<Url>.


3
@Korijn nhìn vào bức tranh i2.iis.net/media/7188126/... từ Giới thiệu về kiến trúc IIS tôi có thể nói rằng tất cả các yêu cầu trong IIS được xử lý theo cách không đồng bộ
Viktor Lova

7
Vì bạn không hành động trên kết quả được đặt trong GetAllUrlsByUserphương thức, bạn không cần phải làm cho nó không đồng bộ. Chỉ cần trả lại Tác vụ và tự lưu một máy trạng thái không cần thiết do trình biên dịch tạo ra.
Johnathon Sullinger

1
@JohnathonSullinger Mặc dù điều đó sẽ hoạt động trong dòng chảy vui vẻ, nhưng điều đó không có tác dụng phụ là bất kỳ ngoại lệ nào sẽ không xuất hiện ở đây và lan truyền đến nơi đầu tiên đang chờ đợi? (Đó không phải là điều xấu nhất định, nhưng đó là một sự thay đổi trong hành vi?)
Henry Bạn đang ở vào

9
Điều thú vị là không ai để ý rằng ví dụ mã thứ 2 trong "About async / await" là hoàn toàn không hợp lý, vì nó sẽ ném ra một ngoại lệ vì cả EF và EF Core đều không an toàn cho luồng, vì vậy cố gắng chạy song song sẽ chỉ ném ra một ngoại lệ
Tseng

1
Mặc dù câu trả lời này đúng, tôi khuyên bạn nên tránh sử dụng asyncawaitnếu bạn KHÔNG làm gì với danh sách. Hãy để người gọi đến awaitnó. Khi bạn chờ cuộc gọi ở giai đoạn này, return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();bạn đang tạo thêm một trình bao bọc không đồng bộ khi bạn dịch ngược assembly và xem IL.
Ali Khakpouri

10

Có một sự khác biệt lớn trong ví dụ bạn đã đăng, phiên bản đầu tiên:

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

Điều này thật tệ , về cơ bản, nó select * from tabletrả về tất cả các kết quả vào bộ nhớ và sau đó áp dụng wherechống lại kết quả đó trong bộ sưu tập bộ nhớ thay vì làm select * from table where...với cơ sở dữ liệu.

Phương thức thứ hai sẽ không thực sự truy cập vào cơ sở dữ liệu cho đến khi một truy vấn được áp dụng cho IQueryable(có thể thông qua .Where().Select()thao tác kiểu linq sẽ chỉ trả về các giá trị db phù hợp với truy vấn.

Nếu các ví dụ của bạn có thể so sánh được, asyncphiên bản thường sẽ hơi chậm hơn theo yêu cầu vì có nhiều chi phí hơn trong máy trạng thái mà trình biên dịch tạo ra để cho phép asyncchức năng.

Tuy nhiên, sự khác biệt chính (và lợi ích) là asyncphiên bản cho phép nhiều yêu cầu đồng thời hơn vì nó không chặn luồng xử lý trong khi chờ IO hoàn thành (truy vấn db, truy cập tệp, yêu cầu web, v.v.).


7
cho đến khi một truy vấn được áp dụng cho IQueryable .... cả IQueryable.Where và IQueryable.Select buộc truy vấn phải thực thi. Cái trước áp dụng một vị từ và cái sau áp dụng một phép chiếu. Nó không được thực thi cho đến khi một toán tử hiện thực hóa được sử dụng, như ToList, ToArray, Single hoặc First.
JJS

0

Câu chuyện ngắn,
IQueryableđược thiết kế để trì hoãn quá trình RUN và trước tiên xây dựng biểu thức kết hợp với các IQueryablebiểu thức khác , sau đó diễn giải và chạy biểu thức nói chung.
Nhưng ToList()phương thức (hoặc một vài loại phương thức như vậy), được đề cập để chạy biểu thức ngay lập tức "nguyên trạng".
Phương thức đầu tiên của bạn ( GetAllUrlsAsync), sẽ chạy ngay lập tức, vì nó được IQueryabletheo sau bởi ToListAsync()phương thức. do đó nó chạy ngay lập tức (không đồng bộ) và trả về một loạt các IEnumerables.
Trong khi đó phương thức thứ hai của bạn ( GetAllUrls), sẽ không được chạy. Thay vào đó, nó trả về một biểu thức và CALLER của phương thức này chịu trách nhiệm chạy biểu thức.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.