IEnumerable и IQueryable

  • Михаил
  • 15 мин. на прочтение
  • 109
  • 15 Nov 2022
  • 15 Nov 2022

При вызове методов LINQ мы только создаем запрос. Его непосредственное выполнение происходит, когда мы начинаем потреблять результаты этого запроса. Нередко это происходит при переборе результата запроса в цикле for или при применении к нему ряда методов - ToList или ToArray, а также если запрос представляет скалярное значение, например, метод Count.

В процессе выполнения запросов LINQ to Entities мы может получать два объекта, которые предоставляют наборы данных: IEnumerable и IQueryable. С одной стороны, интерфейс IQueryable наследуется от IEnumerable, поэтому по идее объект IQueryable это и есть также объект IEnumerable. Но реальность несколько сложнее. Между объектами этих интерфейсов есть разница в плане функциональности, поэтому они не взаимозаменяемы.

Интерфейс IEnumerable находится в пространстве имен System.Collections и System.Collections.Generic (обобщенная версия). Объект IEnumerable представляет набор данных в памяти и может перемещаться по этим данным только вперед. Запрос, представленный объектом IEnumerable, выполняется немедленно и полностью, поэтому получение данных приложением происходит быстро.

При выполнении запроса IEnumerable загружает все данные, и если нам надо выполнить их фильтрацию, то сама фильтрация происходит на стороне клиента.

Интерфейс IQueryable располагается в пространстве имен System.Linq. Объект IQueryable предоставляет удаленный доступ к базе данных и позволяет перемещаться по данным как в прямом порядке от начала до конца, так и в обратном порядке. В процессе создания запроса, возвращаемым объектом которого является IQueryable, происходит оптимизация запроса. В итоге в процессе его выполнения тратится меньше памяти, меньше пропускной способности сети, но в то же время он может обрабатываться чуть медленнее, чем запрос, возвращающий объект IEnumerable.

Для примера используем следующую модель и контекст данных:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public ApplicationContext()
    {
        Database.EnsureDeleted();
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // добавляем один объект
        modelBuilder.Entity<User>().HasData(new User { Id=1, Name="Tom"});
    }
}

Возьмем два вроде бы идентичных выражения. Объект IEnumerable:

int id = 1;
IEnumerable<User> userIEnum = db.Users;
var users=userIEnum.Where(p => p.Id > id).ToList();

Здесь запрос будет иметь следующий вид:

SELECT [u].[Id], [u].[Name]
FROM [Users] AS [u]

Фильтрация результата, обозначенная с помощью метода Where(p => p.Id > id) будет идти уже после выборки из бд в самом приложении.

Чтобы совместить фильтры, нам надо было сразу применить метод Where: db.Users.Where(p => p.Id > id);

Объект IQueryable:

int id = 1;
IQueryable<User> userIQuer = db.Users;
var users=userIQuer.Where(p => p.Id > id).ToList();

Здесь запрос будет иметь следующий вид:

SELECT [u].[Id], [u].[Name]
FROM [Users] AS [u]
WHERE [p].[Id] > 1

Таким образом, все методы суммируются, запрос оптимизируется, и только потом происходит выборка из базы данных.

Это позволяет динамически создавать сложные запросы. Например, мы можем последовательно наслаивать в зависимости от условий выражения для фильтрации:

IQueryable<User> userIQuer = db.Users;
userIQuer = userIQuer.Where(p => p.Id < 7);
userIQuer = userIQuer.Where(p => p.Name == "Tom");
var users = userIQuer.ToList();

В данном случае будет создаваться следующий SQL-запрос:

SELECT [u].[Id], [u].[Name]
FROM [Users] AS [u]
WHERE ([p].[Id] < 7) AND (([u].[Name] = N'Tom')

Что же лучше использовать? Все зависит от конкретной ситуации. Если разработчику нужен весь набор возвращаемых данных, то лучше использовать IEnumerable, предоставляющий максимальную скорость. Если же нам не нужен весь набор, а то только некоторые отфильтрованные данные, то лучше применять IQueryable.