Перейти к основному содержимому

5.05. Работа с БД и ORM

Разработчику Архитектору

Работа с БД и ORM

ORM и Entity Framework Core

Что такое ORM
Настройка контекста, миграции

Работа с БД – одна из ключевых задач при разработке приложений на языке C#. Платформа предоставляет богатый набор инструментов, позволяющих взаимодействовать с реляционными базами данных различными способами, в зависимости от требований проекта, уровня абстракции, производительности и удобства использования:

  • ADO.NET – низкоуровневый, гибкий и производительный способ работы с БД через SQL-запросы;
  • LINQ – мощный механизм запросов, встроенный в C#, позволяющий работать с данными декларативно;
  • LINQ to SQL – легковесный ORM для работы с Microsoft SQL Server;
  • Entity Framework (EF) / EF Core – полноценный ORM, поддерживающий миграции, отслеживание изменений и работу с множеством СУБД;
  • LINQ to DB – высокопроизводительный ORM, сочетающий гибкость ADO.NET и удобство LINQ.

★ Давайте для начала определим общий алгоритм работы с БД.

  1. Создать или выбрать БД, определить СУБД, таблицы, поля, связи и ограничения.
  2. Настроить параметры подключения – указать адрес сервера, имя базы данных, логин, пароль и другие параметры, если требуется. Эти параметры обычно хранятся в конфигурационном файле (appsettings.json, App.config или Web.config) или внедряются через зависимости.
  3. Установка подключения к базе данных – нужно открыть соединение с БД, проверить состояние перед началом работы. В ADO.NET это управляется вручную, в отличие от прочих фреймворков.
  4. Формирование и выполнение запросов – нужно подготовить SQL-запрос или LINQ-выражение, выбрав подходящий метод выполнения – чтение (SELECT), изменение (INSERT, UPDATE, DELETE), или получение одного значения (ExecuteScalar).
  5. Получить и обработать результаты – если запрос возвращает данные – извлечь их и преобразовать в нужный формат (объекты, коллекции, строки и т.д.), обработать возможные ошибки и исключения.
  6. Сохранение изменений – если мы добавляли, изменяли или удаляли данные, нужно убедиться в сохранении, при необходимости использовать транзакции.
  7. Закрыть соединение, чтобы освободить ресурсы.

Этот алгоритм универсален для всех, разница лишь в том, как именно реализуется каждый шаг:

  • в ADO.NET всё контролируется вручную;
  • в LINQ to SQL и EF логика абстрагирована, и многое делается автоматически;
  • в LINQ to DB – баланс между производительностью и удобством.

Как подключиться к базе данных в С#

В C# подключение к базе данных происходит с помощью ADO.NET, который предоставляет богатый набор классов для доступа к базам данных. Для подключения создается объект Connection, указывается строка подключения, и затем с помощью объектов Command выполняются запросы SQL или хранящиеся процедуры. В конце подключение закрывается и обрабатываются любые возникшие исключения.

ADO.NET (ActiveX Data Objects .NET) – набор классов в .NET Framework, которые позволяют напрямую взаимодействовать с базами данных через SQL-запросы. Это безопасный, гибкий и производительный способ работы с БД без абстракций ORM. ADO.NET не создаёт объектов из таблиц автоматически – разработчик самостоятельно управляет подключениями, командами, данными и их обработкой.

Основные компоненты ADO.NET:

КомпонентОписание
ConnectionУстанавливает соединение с базой данных
CommandВыполняет SQL-запрос или хранимую процедуру
DataReaderОбеспечивает последовательное чтение данных в режиме только для чтения
DataAdapterЗаполняет DataSet данными и позволяет синхронизировать изменения с источником
DataSetКэшированная, отключенная от источника копия данных, поддерживающая таблицы, связи и ограничения
TransactionГруппирует команды в единую атомарную операцию (ACID)

Каждый тип СУБД имеет свой провайдер:

ПровайдерСУБД
System.Data.SqlClientMicrosoft SQL Server
System.Data.OleDbИсточники через OLE DB (например, Access, старые версии Oracle)
System.Data.OdbcЛюбые СУБД с ODBC-драйвером
MySql.Data.MySqlClientMySQL
NpgsqlPostgreSQL

Примеры использования ADO.NET

Шаг 1: подключение к БД:

string connectionString = "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;";
using (SqlConnection connection = new SqlConnection(connectionString)) {
connection.Open();
Console.WriteLine("Подключено к БД");
}

Шаг 2а: Выполнение запроса на выборку (SELECT):

string query = "SELECT Id, Name FROM Users";

using (SqlConnection connection = new SqlConnection(connectionString)) {
SqlCommand command = new SqlCommand(query, connection);
connection.Open();

using (SqlDataReader reader = command.ExecuteReader()) {
while (reader.Read()) {
int id = reader.GetInt32(0); // Индекс колонки
string name = reader.GetString(1);
Console.WriteLine($"ID: {id}, Имя: {name}");
}
}
}

Шаг 2б: выполнение запроса на изменение (INSERT, UPDATE, DELETE):

string query = "INSERT INTO Users (Name, Email) VALUES (@name, @email)";

using (SqlConnection connection = new SqlConnection(connectionString)) {
SqlCommand command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@name", "Alice");
command.Parameters.AddWithValue("@email", "alice@example.com");

connection.Open();
int rowsAffected = command.ExecuteNonQuery();
Console.WriteLine($"{rowsAffected} записей добавлено.");
}

Шаг 2в: использование параметров (защита от SQL-инъекций):

command.Parameters.Add("@age", SqlDbType.Int).Value = 30;
// или
command.Parameters.AddWithValue("@age", 30);

Шаг 2г: получение одного значения (ExecuteScalar):

string query = "SELECT COUNT(*) FROM Users";
SqlCommand command = new SqlCommand(query, connection);

object result = command.ExecuteScalar();
int count = Convert.ToInt32(result);
Console.WriteLine($"Всего пользователей: {count}");

Шаг 2д: работа с DataSet и DataAdapter (загрузка данных в память):

string query = "SELECT * FROM Products";
DataSet ds = new DataSet();

using (SqlConnection connection = new SqlConnection(connectionString)) {
SqlDataAdapter adapter = new SqlDataAdapter(query, connection);
adapter.Fill(ds, "Products");

foreach (DataRow row in ds.Tables["Products"].Rows) {
Console.WriteLine(row["ProductName"]);
}
}

Шаг 2е: асинхронное выполнение запросов:

async Task<int> InsertUserAsync(string name, string email) {
string query = "INSERT INTO Users (Name, Email) VALUES (@name, @email)";

using (SqlConnection connection = new SqlConnection(connectionString)) {
SqlCommand command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@name", name);
command.Parameters.AddWithValue("@email", email);

await connection.OpenAsync();
return await command.ExecuteNonQueryAsync();
}
}

ORM (Object Relational Mapping) – технология, которая связывает объектно-ориентированную модель программы с реляционной моделью базы данных. Это упрощает работу с БД через ОПП, избавляет от необходимости писать SQL-запросы вручную, обеспечивает типобезопасность и поддержку LINQ, а также автоматизирует CRUD-операции.

LINQ (Language Integrated Query) – это встроенный в C# механизм запросов к различным источникам данных: коллекциям, массивам, XML, БД и т.д.

Пример простого LINQ-запроса:

var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();

Когда используется вместе с ORM, LINQ-запросы преобразуются в SQL на лету и выполняются на СУБД.

LINQ to SQL – легковесный ORM, позволяющий работать с SQL Server через LINQ-запросы.

Ключевые компоненты:

КомпонентОписание
DataContextОсновной класс LINQ to SQL, обеспечивающий соединение с базой данных и управление операциями чтения/записи
[Table]Атрибут, сопоставляющий класс с таблицей в базе данных (указывается на уровне класса)
[Column]Атрибут, сопоставляющий свойство класса с колонкой в таблице (поддерживает указание имени, типа, ключа и др.)
GetTable<T>()Метод DataContext, возвращающий объект ITable<T>, представляющий таблицу как коллекцию сущностей для выполнения LINQ-запросов

Пример использования LINQ to SQL:

Шаг 1: определение класса, соответствующего таблице:

[Table(Name = "Customers")]
public class Customer {
[Column(IsPrimaryKey = true)]
public int Id { get; set; }

[Column]
public string Name { get; set; }
}

Шаг 2: работа с данными:

string connectionString = "...";
using (DataContext db = new DataContext(connectionString)) {
var customers = db.GetTable<Customer>();

// Выборка
var query = from c in customers
where c.Name.StartsWith("A")
select c;

foreach (var c in query) {
Console.WriteLine(c.Name);
}

// Добавление
Customer newCustomer = new Customer { Name = "Alice" };
customers.InsertOnSubmit(newCustomer);
db.SubmitChanges();
}

LINQ to DB – высокопроизводительная альтернатива EF и LINQ to SQL. Это гибрид ORM и генератора SQL-запросов. У него очень высокая производительность, поддержка множества СУБД, полная интеграция с LINQ, минимальное количество абстракций.

Основные классы:

КомпонентОписание
DataConnectionБазовый класс в LINQ to DB, предоставляющий соединение с базой данных и методы для выполнения запросов
[Table]Атрибут, сопоставляющий класс с таблицей в базе данных (указывается на уровне класса)
[Column]Атрибут, сопоставляющий свойство или поле класса с колонкой в таблице
[PrimaryKey]Атрибут, указывающий, что свойство представляет первичный ключ таблицы

Пример работы с LINQ to DB

Шаг 1: Определение модели:

[Table(Name = "Users")]
public class User {
[PrimaryKey, Identity]
public int Id { get; set; }

[Column]
public string Name { get; set; }
}

Шаг 2: Контекст БД:

public class MyDb : DataConnection {
public ITable<User> Users => GetTable<User>();

public MyDb() : base("MyConnectionString") {}
}

Шаг 3: Использование:

using (var db = new MyDb()) {
// Добавление
db.Users.Insert(() => new User { Name = "Bob" });

// Запрос
var users = db.Users.Where(u => u.Name.StartsWith("B")).ToList();

// Обновление
db.Users.Where(u => u.Id == 1)
.Set(u => u.Name, "Alice")
.Update();

// Удаление
db.Users.Delete(u => u.Id == 1);
}

В реляционных БД можно использовать определенные типы связей – один ко многим, многие ко многим и один к одному.

One-to-Many (Один ко многим) – самый распространённый тип связи. Один автор может написать много книг, но каждая книга имеет только одного автора. В EF такая связь реализуется через навигационные свойства и внешние ключи.

Many-to-Many (многие ко многим) – к примеру, студент может посещать много курсов, курс может содержать много студентов. В реляционных БД такая связь реализуется через соединительную таблицу. Соединительная таблица может содержать дополнительные поля (например, дату зачисления), а запросы с такими связями могут создавать «картезианский взрыв» (много дублирующихся данных).

One-to-One (один к одному) – пользователь имеет один профиль, профиль принадлежит одному пользователю. Используется для разделения больших таблиц, может быть и необязательной связью, а также внешний ключ может находиться в любой из таблиц.

Entity Framework — это ORM-фреймворк, созданный Microsoft. Он позволяет разработчикам работать с базами данных с помощью принципов ООП вместо написания необработанных SQL запросов. Entity Framework автоматически сопоставляет таблицы базы данных с классами и даёт такие функции, как поддержка LINQ, отслеживание изменений и CRUD операции. Он упрощает доступ и изменение баз данных в приложениях C#.

Основные компоненты:

КомпонентОписание
DbContextОсновной класс Entity Framework, представляющий сессию с базой данных; управляет подключением, отслеживанием сущностей и сохранением изменений
DbSet<TEntity>Представляет коллекцию сущностей типа TEntity, соответствующую таблице в базе данных; используется для запросов и манипуляций с данными
OnModelCreating()Метод, который можно переопределить для настройки модели с помощью Fluent API (например, связи, индексы, имена таблиц)
SaveChanges()Сохраняет все изменения, отслеживаемые контекстом, в базу данных; выполняет INSERT, UPDATE, DELETE операции
MigrationsМеханизм управления версиями схемы базы данных; позволяет создавать и применять миграции на основе изменений в модели (Add-Migration, Update-Database)
LINQЯзык интегрированных запросов, используемый для построения типобезопасных запросов к данным через DbSet, которые транслируются в SQL

Давайте разберём некоторые компоненты EF и их назначение.

EF состоит из нескольких ключевых компонентов, каждый из которых играет свою роль в работе с базой данных.

  1. EF Designer - графический инструмент для создания модели данных. Позволяет визуально проектировать модель (например, в подходе Model First). Автоматически генерирует классы и контекст на основе диаграммы. Разработчик сначала создаёт диаграмму в Visual Studio, а EF Designer генерирует SQL-скрипты для создания БД и классы для работы.
  2. EF Tools - инструменты командной строки и интерфейса для управления миграциями. Нужно для добавления, применения, отката миграций, создания скриптов для БД.
  3. Migrations - механизм управления изменениями структуры базы данных. Нужен для последовательного обновления БД, поддержки версионирования и отката изменений. Каждая миграция представляет собой шаг в эволюции базы данных, а EF хранит историю миграций в таблице __EFMigrationsHistory.
  4. DbContext - центральный компонент EF, который управляет взаимодействием с базой данных. Он предоставляет доступ к таблицам через свойства DbSet, управляет транзакциями и состоянием объектов. Разработчик создаёт класс, наследующий DbContext, и определяет в нём свойства DbSet.
  5. LINQ - язык запросов. LINQ является важной частью, так как предоставляет готовый набор решения для «общения» с базами данных.

★ Пример работы с EF:

Шаг 1: Создание модели:

public class Product {
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}

Шаг 2: Контекст БД:

public class AppDbContext : DbContext {
public DbSet<Product> Products { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
optionsBuilder.UseSqlServer("Server=...;Database=MyDB;Trusted_Connection=True;");
}
}

Шаг 3: Использование:

using (var db = new AppDbContext()) {
// Добавление
var product = new Product { Name = "Laptop", Price = 1200 };
db.Products.Add(product);
db.SaveChanges();

// Запрос
var expensiveProducts = db.Products.Where(p => p.Price > 1000).ToList();

// Обновление
product.Price = 1100;
db.SaveChanges();

// Удаление
db.Products.Remove(product);
db.SaveChanges();
}

★ Модели данных в EF представляют собой классы C#, которые описывают структуру таблиц в БД. Эти классы используются для работы с данными через ORM.

Сначала нужно опеределить классы (сущности). Каждый класс соответствует таблице в БД, а свойства класса соответствуют столбцам таблицы.

Пример:

public class User
{
public int Id { get; set; } // Первичный ключ
public string Name { get; set; }
public int Age { get; set; }
}

public class Order
{
public int Id { get; set; }
public string ProductName { get; set; }
public int UserId { get; set; } // Внешний ключ
public User User { get; set; } // Навигационное свойство
}

Затем нужно настроить контекст (DbContext) - создать класс, наследующий DbContext. Он управляет взаимодействием с базой данных. Для каждой сущности нужно добавить свойства DbSet<T>:

public class ApplicationContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("YourConnectionStringHere");
}
}

Потом добавляются атрибуты или Fluent API (опционально), при помощи аннотаций - для настройки маппинга. Затем производится миграция для создания базы данных на основе модели.

★ Атрибуты бывают разных видов.

Атрибуты для первичного ключа - [Key], он указывает, что свойство является первичным ключом (PRIMARY KEY).

[Key]
public int Id { get; set; }

Атрибуты для обязательных или необязательных полей. [Required] равнозначен NOT NULL - обозначает, что поле обязательно для заполнения.

Если без [Required], значит поле может быть NULL. В таком случае, если тип nullable, нужно добавить вопросительный знак.

Пример:

[Required]
public string Name { get; set; }

public string? Address { get; set; } // NULL в SQL

Атрибуты для длины и типа данных:

  • [MaxLength(100)] равнозначен NVARCHAR(100) - ограничение длины строки;
  • [Column(TypeName = “varchar(50)”)] равнозначен VARCHAR(50) - точное указание типа SQL;
  • [Precision(18, 2)] равнозначен DECIMAL(18,2) - для чисел с фиксированной точностью. Пример:
[MaxLength(100)]
public string Email { get; set; }

[Column(TypeName = "decimal(18,2)")]
public decimal Salary { get; set; }

Атрибуты для связей между таблицами:

  • [ForeignKey(“AuthorId”)] равнозначен FOREIGN KEY (AuthorId), указывает свойство-внешний ключ;
  • [InverseProperty(“Books”)] - навигационное поле для связи.

Пример (один-ко-многим):

public class Author
{
[Key]
public int Id { get; set; }
public List<Book> Books { get; set; } // Навигационное свойство
}

public class Book
{
[Key]
public int Id { get; set; }
public int AuthorId { get; set; } // Внешний ключ

[ForeignKey("AuthorId")]
public Author Author { get; set; } // Навигационное свойство
}

Атрибуты для индексов:

  • [Index] равнозначен CREATE INDEX - создаёт некластеризованный индекс;
  • [Index(IsUnique = true)] равнозначен CREATE UNIQUE INDEX - уникальный индекс.

ADO.NET используется в качестве фундамента для работы с базой данных в Entity Framework. Однако EF абстрагирует большинство низкоуровневых операций ADO.NET, предоставляя более удобный интерфейс.

ADO.NET — это низкоуровневый подход, предоставляющий прямой доступ к базе данных через объекты, такие как SqlConnection, SqlCommand, SqlDataReader и т.д., а разработчик вручную пишет SQL-запросы и управляет подключением. Создаётся переменная, равная новому экземпляру класса SqlConnection с указанием базы данных. Потом вызывается метод Open, а затем уже создаются переменные для SqlCommand с текстом запроса и так далее.

Вот пример:

using (var connection = new SqlConnection("YourConnectionStringHere"))
{
connection.Open();
var command = new SqlCommand("SELECT * FROM Users", connection);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine(reader["Name"]);
}
}
}

EF же высокоуровневый подход, использует ADO.NET «под капотом» для выполнения SQL-запросов. Разработчик работает с объектами (классами) и LINQ-запросами, а EF автоматически их преобразует в SQL.

Миграции

Миграции в Entity Framework выполняются через команды в консоли диспетчера пакетов (Package Manager Console) или терминале. Вот пошаговый процесс.

  1. Настройка проекта - нужно подключить необходимые пакеты NuGet:
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.SqlServer
  1. Создание модели. Нужно определить классы, которые будут представлять таблицы в базе данных, к примеру:

класс User:

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}

класс ApplicationContext:

public class ApplicationContext : DbContext
{
public DbSet<User> Users { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("YourConnectionStringHere");
}
}
  1. Добавление миграции. В консоли диспетчера пакетов нужно выполнить команду для добавления миграции:
Add-Migration InitialCreate

Эта команда создаст файл миграции в папке Migrations. В нём будет описано, как изменить базу данных (например, создать таблицу Users).

  1. Применение миграции. Выполните команду для применения миграции к БД:
Update-Database

База данных будет обновлена в соответствии с изменениями.

  1. Откат миграции. Если нужно откатить последнюю миграцию:
Remove-Migration

Или откатить все миграции до определённой:

Update-Database LastGoodMigrationName

Работа с EF через CLI

Миграции можно выполнять как через консоль диспетчера пакетов, так и через CLI.

Установка:

dotnet tool install --global dotnet-ef

Обновление:

dotnet tool update --global dotnet-ef

Добавление миграции:

dotnet ef migrations add <MigrationName>

<MigrationName> — имя миграции (например, InitialCreate, AddUsersTable).

Эта команда создает файл миграции в папке Migrations вашего проекта.

Пример:

dotnet ef migrations add AddUsersTable

Применение миграции к базе данных:

dotnet ef database update

Применение конкретной миграции:

dotnet ef database update <MigrationName>

Откат миграции:

dotnet ef migrations remove

Эта команда удаляет последнюю миграцию из папки Migrations и отменяет её применение.

Чтобы увидеть список всех миграций:

dotnet ef migrations list

Для удаления базы данных:

dotnet ef database drop

Можно также сгенерировать SQL-скрипт:

dotnet ef migrations script

…и создать модель из существующей базы данных:

dotnet ef dbcontext scaffold

CLI особенно удобен для автоматизации процессов и работы в кроссплатформенных средах.

Как создавать модели через Model First?

Проектирование Model First в EF через Visual Studio

  1. Создание нового проекта.
  2. Добавление модели ADO.NET Entity Data Model:
    • ПКМ на проекте - Add - New Item
    • выбрать ADO.NET Entity Data Model
    • выбрать Empty EF Designer Model.

Модель будет иметь расширение .edmx

  1. Проектирование модели:
    • откроется графический редактор (EF Designer);
    • нужно будет добавить сущности (User, Order) через панель инструментов;
    • определить свойства сущностей (Id, Name, Age);
    • настроить связи между сущностями (например, один-ко-многим).
  2. Генерация кода. После завершения проектирования, EF автоматически сгенерирует классы для каждой сущности и контекст (DbContext) для работы с БД.
  3. Создание БД:
    • в свойствах модели нужно выбрать, какую СУБД использовать (например, SQL Server);
    • нажать на генерацию (Generate Database from Model);
    • EF создаст SQL-скрипт для создания БД;
    • этот скрипт нужно выполнить в СУБД.
  4. Использование модели. После этого можно использовать сгенерированные классы в коде.

В коде использование идёт по принципу:

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

var context = new DbContext

объявляется переменная, равная новому экземпляру класса таблицы:

var user = new User { Name = "Alice", Age = 30 };

управление происходит через формат <контекст>.<таблица>.<метод/свойство>:

context.Users.Add(user);
context.SaveChanges();

Альтернатива EF

Для сложных запросов используется "сырой" SQL через метод context.Database.ExecuteSqlRaw().

Dapper - микро-ORM, который фокусируется на производительности.

В отличие от EF, основного ORM для .NET, интегрированного с экосистемой Microsoft, Dapper имеет минимальную абстракцию и высокую скорость выполнения запросов. Его можно встретить в проектах, где важна производительность и контроль над SQL.

LINQ в EF Core

Отслеживание изменений (Change Tracking)
Навигационные свойства
Ленивая загрузка (Lazy Loading), явная, Eager Loading

Постраничный вывод
Skip, Take в LINQ
Реализация пагинации

★ Методы при работе с БД

Метод/свойство LINQ (EF)SQL-эквивалентОписание
.Where()WHEREФильтрация записей по условию.
.Select()SELECT (проекция)Выбор конкретных полей или преобразование данных.
.OrderBy()ORDER BY ... ASCСортировка результатов по возрастанию.
.OrderByDescending()ORDER BY ... DESCСортировка результатов по убыванию.
.ThenBy() / .ThenByDescending()ORDER BY x, yДополнительная сортировка после основной.
.First() / .FirstOrDefault()SELECT TOP 1 ...Получение первой записи; First() — с исключением при отсутствии, FirstOrDefault() — возвращает null.
.Single() / .SingleOrDefault()SELECT TOP 2 ... (с проверкой количества)Получение единственной записи; Single() ожидает ровно один результат, иначе — исключение.
.Count()COUNT(*)Подсчёт количества записей, удовлетворяющих условию.
.Any()EXISTSПроверка наличия хотя бы одной записи, удовлетворяющей условию.
.All()NOT EXISTS (с условием NOT)Проверка, что все записи в наборе удовлетворяют заданному условию.
.Sum() / .Average()SUM() / AVG()Вычисление суммы или среднего значения числовой колонки.
.Min() / .Max()MIN() / MAX()Получение минимального или максимального значения.
.Skip() / .Take()OFFSET ... FETCH NEXT ... ROWS ONLYРеализация пагинации: пропуск первых N записей и выбор следующих M.
.Include()JOIN (жадная загрузка)Загрузка связанных сущностей (навигационных свойств).
.GroupBy()GROUP BYГруппировка записей по ключу с возможностью агрегации.
.Join()INNER JOINВнутреннее соединение двух коллекций по ключу.
.Distinct()DISTINCTУдаление дублирующихся записей из результата.
.FromSqlRaw()Прямой SQL-запросВыполнение "сырого" SQL-запроса напрямую к базе данных.
ToListAsync() SaveChangesAsync()SELECT * FROM Table INSERT/UPDATE/DELETE (асинхронный)Асинхронное выполнение операций чтения и сохранения изменений.
ToList() ToArray() ToDictionary()SELECT * FROM TableМатериализация запроса: выполнение в БД и загрузка результатов в память.
BeginTransaction() Database.BeginTransaction() Commit() Rollback()BEGIN TRANSACTION COMMIT ROLLBACKУправление транзакциями: начало, фиксация, откат.
ExecuteProc() FromSqlRaw()EXEC ProcedureNameВызов хранимых процедур.
AddRange() RemoveRange()INSERT INTO ... VALUES (...) DELETE FROM ... WHERE Id IN (...)Массовое добавление или удаление сущностей.

Стратегии загрузки данных

Есть три стратегии загрузки связанных данных в EF - ленивая, жадная и явная.

Представим, что у нас есть класс User (пользователь), класс Order (заказ). Каждый User может иметь много Order. Это связанные таблицы. Как загрузить пользователя и его заказы?

Ленивая загрузка (Lazy Loading) - подход, при котором связанные данные (например, заказы пользователя) загружаются автоматически только при первом обращении к навигационному свойству.

Мы запрашиваем пользователя:

var user = db.Users.First(); // SELECT * FROM Users LIMIT 1

Пока мы не обратились к user.Orders, заказы не загружаются.

Но как только мы пишем:

var orders = user.Orders; // SELECT * FROM Orders WHERE UserId = 1

EF самостоятельно выполняет дополнительный запрос в БД.

При таком подходе не нужно заранее думать, какие данные нам понадобятся. Однако возникает проблема, называемая N+1 - если у нас 100 пользователей, EF сделает 101 запрос (1 для пользователей + 100 для заказов).

Запросил пользователя → Потом запросил заказы → Получил N+1.

EF использует прокси-классы (динамически создаваемые наследники классов), которые переопределяют навигационные свойства. При обращении к свойству, например, user.Orders, прокси выполняет SQL-запрос на лету.

Для этого нужно установить Microsoft.EntityFrameworkCore.Proxies:

dotnet add package Microsoft.EntityFrameworkCore.Proxies

Затем понадобится включить прокси в DbContext:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseLazyLoadingProxies() // Включаем ленивую загрузку
.UseSqlServer("Your_Connection_String");
}

Навигационные свойства должны быть виртуальными (virtual):

public class User
{
public int Id { get; set; }
public virtual ICollection<Order> Orders { get; set; } // virtual обязательно!
}

Пример:

var user = db.Users.First();  // SELECT * FROM Users LIMIT 1

// Заказы загружаются только здесь (лениво):
var orders = user.Orders; // SELECT * FROM Orders WHERE UserId = 1

Жадная загрузка (Eager Loading) предварительно загружает связанные данные одним запросом с помощью метода .Include(). Данные загружаются сразу при выполнении основного запроса, и используется JOIN или дополнительные запросы (например, Include().ThenInclude()).

Мы явно говорим EF «Мне нужны пользователи и их заказы»:

var users = db.Users
.Include(u => u.Orders) // Жадно грузим заказы
.ToList();

EF делает один запрос с JOIN (или несколько запросов, но сразу).

В отличие от ленивой загрузки, здесь выполняется 1-2 запроса, а не сотни, таким образом, избавляясь от проблемы N+1. Но при таком подходе можно загрузить слишком много, потому что мы загрузили Orders целиком.

Запросил пользователя + заказы сразу → 1 запрос.

Пример:

var users = db.Users
.Include(u => u.Orders) // Жадная загрузка заказов
.ThenInclude(o => o.Items) // Загрузка товаров в заказах
.ToList();

SQL-эквивалент:

-- Вариант 1: JOIN (по умолчанию)
SELECT * FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId
LEFT JOIN Items i ON o.Id = i.OrderId

-- Вариант 2: Отдельные запросы (если настроено в контексте)
SELECT * FROM Users;
SELECT * FROM Orders WHERE UserId IN (1, 2, ...);
SELECT * FROM Items WHERE OrderId IN (10, 20, ...);

Явная загрузка (Explicit Loading) помогает вручную загрузить связанные данные после загрузки основного объекта. Используются методы .Collection() для коллекций и .Reference() для одиночных объектов. Требует явного вызова .Load().

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

var user = db.Users.First(); // SELECT * FROM Users LIMIT 1

А потом вручную говорим «Теперь загрузи его заказы»:

db.Entry(user)
.Collection(u => u.Orders)
.Load(); // SELECT * FROM Orders WHERE UserId = 1

В таком подходе мы получаем полный контроль - решаем сами, когда и что грузить, не делая лишних запросов, если данные не нужны. Минус - код длиннее (нужно писать .Load()).

Запросил пользователя → Потом решил загрузить заказы → 2 запроса.

Пример:

var user = db.Users.First();  // SELECT * FROM Users LIMIT 1

// Явная загрузка заказов:
db.Entry(user)
.Collection(u => u.Orders)
.Load(); // SELECT * FROM Orders WHERE UserId = 1

// Для одиночных связей (например, User.Profile):
db.Entry(user)
.Reference(u => u.Profile)
.Load();

Таким образом:

  • ленивая загрузка загружает при первом обращении, имеет небольшой риск потери производительности, но зато автоматическая - и требует виртуальности свойств, подойдёт для простых сценариев вроде админок или прототипов;
  • жадная загрузка загружает сразу в основном запросе, имеет оптимальную производительность, удобная для сложных запросов, подойдёт для API, когда важна производительность - ибо всё запрошенное выгрузится в память;
  • явная загрузка загружает по требованию через Load(), производительность полностью в руках разработчика - ручное управление, подойдёт тогда, когда заранее не знаем, какие данные понадобятся.