Перейти к содержанию
Educora
Продвинутый24 мин15 / 16

Entity Framework Core: работа с базами данных

Создавай таблицы из классов с EF Core: `DbContext`, миграции, LINQ to Entities, CRUD-операции, связи и типичные ловушки производительности.

Проверь себя
В этом уроке ты узнаешь
  • Писать классы модели и DbContext, создавать и применять миграции
  • Читать, добавлять, изменять и удалять данные через LINQ
  • Настраивать связи и избегать проблемы N+1 и SQL-инъекций

С низкоуровневым ADO.NET каждый SQL-запрос приходится писать вручную и переносить каждый столбец в объект. Entity Framework Core (EF Core), официальная ORM для .NET, берёт эту работу на себя: ты пишешь классы, EF Core создаёт для них таблицы, переводит запросы LINQ в SQL и собирает результаты в объекты. EF Core работает с SQL Server, PostgreSQL, MySQL и SQLite; в примерах мы используем SQLite, который не требует установки.

  1. 1
    Добавь пакеты

    Microsoft.EntityFrameworkCore.Sqlite — провайдер SQLite, а Microsoft.EntityFrameworkCore.Design нужен инструментам миграций.

  2. 2
    Установи инструмент dotnet-ef

    dotnet tool install --global dotnet-ef добавляет команды dotnet ef; достаточно сделать это один раз на компьютере.

  3. 3
    Напиши модель и DbContext

    Каждая таблица — это класс, а DbContext — «дверь» в базу данных.

  4. 4
    Создай и примени миграцию

    dotnet ef migrations add InitialCreate записывает схему в виде C#-файла, а dotnet ef database update применяет её к базе.

Terminal
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef
Выполняется в папке проекта (пакеты скачиваются с nuget.org)

Модель и DbContext

EF Core работает по соглашениям: свойство с именем Id — первичный ключ, пара ClassroomId + Classroom — внешний ключ, а List<Student> — связь «в одном классе много учеников». Ключевое слово required говорит, что Name будет столбцом NOT NULL и его обязательно нужно задать при создании объекта. Свойства DbSet<T> у DbContext соответствуют таблицам, а OnConfiguring указывает, к какой базе подключаться.

C#
using Microsoft.EntityFrameworkCore;

public class Classroom
{
    public int Id { get; set; }                        // primary key by convention
    public required string Name { get; set; }
    public List<Student> Students { get; set; } = [];  // one classroom, many students
}

public class Student
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public int Score { get; set; }
    public int ClassroomId { get; set; }               // foreign key
    public Classroom Classroom { get; set; } = null!;
}

public class SchoolContext : DbContext
{
    public DbSet<Classroom> Classrooms => Set<Classroom>();
    public DbSet<Student> Students => Set<Student>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=school.db");
}
Модель: два класса, одна связь и контекст

В настоящем приложении строку подключения не пишут в OnConfiguring: контекст получает параметры через конструктор — public class SchoolContext(DbContextOptions<SchoolContext> options) : DbContext(options), — а сама строка берётся из файла конфигурации (это ты увидишь в ASP.NET Core в следующем уроке). DbContext — лёгкий короткоживущий объект: его обычно создают на один запрос или одну единицу работы, и он не потокобезопасен, поэтому делить его между параллельными задачами нельзя.

Миграции

Миграция — C#-файл, который переносит изменение модели в базу данных. migrations add сравнивает модель с её прошлым состоянием и создаёт в папке Migrations/ класс с методами Up (применить) и Down (откатить); database update выполняет ещё не применённые миграции. Добавил в класс новое свойство? Создай новую миграцию — например, AddEmail — и обнови базу. Миграции хранятся в Git, поэтому у всех в команде база с одинаковой схемой.

Terminal
dotnet ef migrations add InitialCreate
dotnet ef database update
Ожидаемый результат
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
Build started...
Build succeeded.
Applying migration '20260927101500_InitialCreate'.
Done.
Пример вывода: имя миграции начинается с отметки даты и времени

LINQ to Entities: CRUD-операции

Запрос LINQ, написанный для DbSet<T>, выполняется не в памяти, а в базе: EF Core переводит дерево выражений в SQL (LINQ to Entities). Add и Remove лишь запоминают изменения, а SaveChangesAsync отправляет их в базу одной транзакцией. Объект, прочитанный из базы, отслеживается (change tracking): достаточно изменить его свойство и вызвать SaveChangesAsync — EF Core сам напишет UPDATE.

C#
await using var db = new SchoolContext();

var tenA = new Classroom { Name = "10A" };
tenA.Students.Add(new Student { Name = "Aysel", Score = 95 });
tenA.Students.Add(new Student { Name = "Leyla", Score = 88 });
db.Classrooms.Add(tenA);
await db.SaveChangesAsync();                         // INSERTs; Id values are filled in

List<string> top = await db.Students
    .Where(s => s.Score >= 90)
    .OrderByDescending(s => s.Score)
    .Select(s => s.Name)
    .ToListAsync();                                  // the SQL runs here
Console.WriteLine(string.Join(", ", top));

Student leyla = await db.Students.SingleAsync(s => s.Name == "Leyla");
leyla.Score = 91;                                    // the change tracker notices this
await db.SaveChangesAsync();                         // UPDATE "Students" SET "Score" = ...

db.Students.Remove(leyla);
await db.SaveChangesAsync();                         // DELETE
Console.WriteLine(await db.Students.CountAsync());
Ожидаемый результат
Aysel
1
Пример вывода на пустой базе: 90+ баллов только у Айсель, а после удаления Лейлы остаётся один ученик

Полезно видеть, какой SQL генерирует EF Core: ToQueryString() возвращает текст SQL запроса, не выполняя его.

C#
var query = db.Students
    .Where(s => s.Score >= 80)
    .OrderByDescending(s => s.Score)
    .Select(s => s.Name);
Console.WriteLine(query.ToQueryString());
Ожидаемый результат
SELECT "s"."Name"
FROM "Students" AS "s"
WHERE "s"."Score" >= 80
ORDER BY "s"."Score" DESC
Пример вывода для SQLite; другие провайдеры генерируют немного иной SQL

Связи, Include и проблема N+1

Связанные данные не загружаются автоматически: если student.Classroom или classroom.Students не запрошены в запросе, они остаются пустыми. Include(...) приносит связанные объекты в том же запросе. Для связи «многие ко многим» достаточно коллекции с обеих сторон — промежуточную таблицу EF Core создаст сам. Самая известная ошибка производительности — N+1: один запрос за списком, а потом ещё по запросу на каждый элемент.

C#
public class Course
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public List<Student> Students { get; set; } = [];
}

// in Student:  public List<Course> Courses { get; set; } = [];
// EF Core creates the join table CourseStudent automatically

var withCourses = await db.Students
    .Include(s => s.Courses)                 // load related data in the same query
    .Where(s => s.Courses.Any(c => c.Title == "Math"))
    .ToListAsync();
Связь «многие ко многим»: у ученика много курсов, у курса много учеников
N+1: 1 + N запросов
var classes = await db.Classrooms.ToListAsync();
foreach (var c in classes)
{
    // one more query for every classroom: the N+1 problem
    int count = await db.Students.CountAsync(s => s.ClassroomId == c.Id);
    Console.WriteLine($"{c.Name}: {count}");
}
Проекция: один запрос
var classes = await db.Classrooms
    .Select(c => new { c.Name, Count = c.Students.Count })
    .ToListAsync();                  // a single query with a subquery
foreach (var c in classes)
    Console.WriteLine($"{c.Name}: {c.Count}");
Опасно: склейка строк
var list = await db.Students
    .FromSqlRaw("SELECT * FROM Students WHERE Name = '" + name + "'")
    .ToListAsync();                  // SQL injection!
Безопасно: интерполяция превращается в параметр
var list = await db.Students
    .FromSql($"SELECT * FROM Students WHERE Name = {name}")
    .ToListAsync();                  // {name} becomes a SQL parameter
Когда нужен сырой SQL, FromSql (с EF Core 7) превращает интерполированные значения в параметры SQL

Главное

  • EF Core связывает классы с таблицами: DbContext + DbSet<T>; Id — первичный ключ, XxxId — внешний.
  • Миграции: dotnet ef migrations add записывает схему, dotnet ef database update применяет её; они хранятся в Git.
  • LINQ над DbSet превращается в SQL (его показывает ToQueryString()); изменения сохраняет SaveChangesAsync.
  • Связанные данные загружай одним запросом через Include или проекцию — избегай N+1.
  • Сырой SQL — через FromSql($"..."); для чтения — AsNoTracking(), для массовых изменений — ExecuteUpdateAsync/ExecuteDeleteAsync.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какая команда применяет миграции к базе данных?