Запросы LINQ to Entities

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

Для взаимодействия с источником данных Entity Framework Core использует технологию LINQ to Entities. В основе данной технологии лежит язык интегрированных запросов LINQ (Language Integrated Query). LINQ предлагает простой и интуитивно понятный подход для получения данных с помощью выражений, которые по форме близки выражениям языка SQL.

Хотя при работе с базой данных мы оперируем запросами LINQ, но, к примеру, база данных MS SQL Server понимает только запросы на языке SQL. Поэтому Entity Framework Core, используя выражения LINQ to Entities, транслирует их в определенные запросы, понятные для используемого источника данных.

Создание запросов

Создавать запросы мы можем двумя способами: через операторы LINQ и через методы расширения. Пусть для рассмотрения основных запросов в LINQ to Entities у нас будут следующие модели со связью один-ко-многим:

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List Users { get; set; } = new List();
}
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public int CompanyId { get; set; }
    public Company Company { get; set; }
}

И пусть будет следующий контекст данных:

public class ApplicationContext : DbContext
{
    public DbSet Companies { get; set; }
    public DbSet Users { get; set; }
     
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;");
    }
}

Вначале используем некоторые операторы LINQ:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace HelloApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            using (ApplicationContext db = new ApplicationContext())
            {
                // пересоздаем базу данных
                db.Database.EnsureDeleted();
                db.Database.EnsureCreated();
                Company microsoft = new Company { Name = "Microsoft" };
                Company google = new Company { Name = "Google" };
                db.Companies.AddRange(microsoft, google);
                User tom = new User { Name = "Tom", Age = 36, Company = microsoft };
                User bob = new User { Name = "Bob", Age = 39, Company = google };
                User alice = new User { Name = "Alice", Age = 28, Company = microsoft };
                User kate = new User { Name = "Kate", Age = 25, Company = google };
                db.Users.AddRange(tom, bob, alice, kate);
                db.SaveChanges();
            }
            using (ApplicationContext db = new ApplicationContext())
            {
                var users = (from user in db.Users.Include(p => p.Company)
                             where user.CompanyId == 1
                             select user).ToList();
                foreach (var user in users)
                    Console.WriteLine($"{user.Name} ({user.Age}) - {user.Company.Name}");
            }
        }
    }
}

Вначале здесь происходит добавление данных, если они отсутствуют. Далее промаются Linq-операторы:

var users = (from user in db.Users.Include(p=>p.Company)
              where user.CompanyId == 1
              select user).ToList();

Данный запрос говорит, что из набора db.Users надо выбрать каждый объект в переменную user. Далее с помощью оператора where проводится фильтрация объектов, и если объект соответствует критерию (в данном случае id компании должно равняться 1), то этот объект передается дальше. И в конце оператор select передает выбранные значения в результирующую выборку, которая возвращается LINQ-выражением.

В итоге мы получим следующий консольный вывод:

Tom (36) - Microsoft Alice (28) - Microsoft

Теперь для получения данных используем методы расширения LINQ:

using(ApplicationContext db = new ApplicationContext())
{
    var users = db.Users.Include(p=>p.Company).Where(p=> p.CompanyId == 1);
}

Оба запроса в итоге транслируются в одно и то же выражение sql:

SELECT [p].[Id], [p].[CompanyId], [p].[Name], [p].[Age], [c].[Id], [c].[Name]
            FROM [Users] AS [p]
            INNER JOIN [Companies] AS [c] ON [p].[CompanyId] = [c].[Id]
            WHERE [p].[CompanyId] = 1

Основные методы, которые мы можем использовать для создания запросов в Entity Framework Core:

All / AllAsync: возвращает true, если все элементы набора удовлетворяют определенному условию, иначе возвращает false

Any / AnyAsync: возвращает true, если хотя бы один элемент набора определенному условию

Average / AverageAsync: подсчитывает cреднее значение числовых значений в наборе

Contains / ContainsAsync: определяет, содержит ли набор определенный элемент

Count / CountAsync: подсчитывает количество элементов в наборе

First / FirstAsync: выбирает первый элемент коллекции

FirstOrDefault / FirstOrDefaultAsync: выбирает первый элемент коллекции или возвращает значение по умолчанию

Single / SingleAsync: выбирает единственный элемент коллекции, если коллекция содержит больше или меньше одного элемента, то генерируется исключение

SingleOrDefault / SingleOrDefaultAsync: выбирает первый элемент коллекции или возвращает значение по умолчанию

Select: определяет проекцию выбранных значений

Where: определяет фильтр выборки

OrderBy: упорядочивает элементы по возрастанию

OrderByDescending: упорядочивает элементы по убыванию

ThenBy: задает дополнительные критерии для упорядочивания элементов возрастанию

ThenByDescending: задает дополнительные критерии для упорядочивания элементов по убыванию

Join: соединяет два набора по определенному признаку

GroupBy: группирует элементы по ключу

Except: возвращает разность двух наборов, то есть те элементы, которые содератся только в одном наборе

Union: объединяет два однородных набора

Intersect: возвращает пересечение двух наборов, то есть те элементы, которые встречаются в обоих наборах элементов

Sum / SumAsync: подсчитывает сумму числовых значений в коллекции

Min / MinAsync: находит минимальное значение

Max / MaxAsync: находит максимальное значение

Take: выбирает определенное количество элементов с начала последовательности

TakeLast: выбирает определенное количество элементов с конца последовательности

Skip: пропускает определенное количество элементов с начала последовательности

SkipLast: пропускает определенное количество элементов с конца последовательности

TakeWhile: возвращает цепочку элементов последовательности, до тех пор, пока условие истинно

SkipWhile: пропускает элементы в последовательности, пока они удовлетворяют заданному условию, и затем возвращает оставшиеся элементы

ToList / ToListAsync: получения списка объектов

Для большинства методов определены асинхронные версии, при необходимости получать данные в асинхронном режиме, мы можем их задействовать:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace HelloApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            using (ApplicationContext db = new ApplicationContext())
            {
                var users = await db.Users
                                    .Include(p => p.Company)
                                    .Where(p => p.CompanyId == 1)
                                    .ToListAsync();     // асинхронное получение данных
                foreach (var user in users)
                    Console.WriteLine($"{user.Name} ({user.Age}) - {user.Company.Name}");
            }
            Console.Read();
        }
    }
}