Изменение данных с помощью Entity Framework Core

  • Михаил
  • 12 мин. на прочтение
  • 173
  • 24 Nov 2022
  • 24 Nov 2022

ChangeTracker и состояние объекта в Entity Framework Core

Прежде чем мы начнем изменять данные с помощью Entity Framework Core, мы должны ознакомиться с некоторыми дополнительными функциями EF Core.

Как мы узнали, в первой части серии, DbContext состоит всего из трех свойств: ChangeTracker, Database и Model. Мы видели свойства модели и базы данных в действии в предыдущих статьях, поэтому в этой статье мы поговорим о свойствах ChangeTracker и State.

Свойство ChangeTracker обеспечивает доступ к информации отслеживания изменений и операциям с загруженной в данный момент сущностью. Эта возможность очень важна, когда мы хотим выполнить любую операцию модификации базы данных. EF Core имеет такую ​​информацию (об отслеживании и операциях) независимо от того, создаем ли мы ее, изменяем или удаляем сущность.

Кроме того, EF Core не будет выполнять никаких операций, пока мы не вызовем метод SaveChanges. Поэтому знание того, какую операцию мы хотим выполнить, имеет решающее значение для EF Core до вызова метода SaveChanges.

Каждой отслеживаемой сущности присвоено свойство State. Когда мы используем объект контекста для загрузки сущности без метода AsNoTracking или меняем ее состояние с помощью методов Update, Remove или Add, эта сущность будет отслеживаемой сущностью. Значение свойства State можно получить с помощью команды _context.Entry(our_entity).State.

Вот возможные состояния отслеживаемых объектов:

  • Detached - объект не отслеживается, и вызов метода SaveChanges не даст никакого эффекта.
  • Unchanged - объект загружен из базы данных, но не имеет изменений. Метод SaveChanges игнорирует его.
  • Added - объект не существует в базе данных, и вызов метода SaveChanges добавит его в базу данных.
  • Modified - объект существует в базе данных и был изменен, поэтому вызов метода SaveChanges изменит его в базе данных.
  • Deleted - объект существует в базе данных, и как только мы вызовем метод SaveChanges, он будет удален из базы данных.

Теперь, когда мы все это знаем, мы можем перейти к операциям Create, Update и Delete.

Добавление данных с помощью методов Add и AddRange

Мы можем создавать новые строки в базе данных с помощью методов Add и AddRange. Метод Add изменяет состояние отдельной сущности, где AddRange может делать то же самое, но для нескольких сущностей. Давайте посмотрим, как работает метод Add после того, как мы отправим объект student из Postman:

[HttpPost]
public IActionResult Post([FromBody] Student student)
{
    if (student == null)
        return BadRequest();

    if (!ModelState.IsValid)
        return BadRequest();

    _context.Add(student);
    _context.SaveChanges();

    return Created("URI of the created entity", student);
}

В этом действии мы используем атрибут HttpPost, чтобы пометить его как действие POST. Кроме того, мы загружаем сущность студента с атрибутом FromBody и добавляем в наш код две проверки.

После этих проверок мы вызываем метод Add, чтобы изменить состояние объекта на «Добавлено», и вызываем метод SaveChanges для создания новой строки в базе данных. Последняя строка кода вернет созданную сущность (с ее идентификатором) клиенту:

Это результат в базе данных:

Отслеживание изменений при добавлении объекта

Если мы немного изменим наш код, мы сможем проверить, как изменяется состояние объекта во время этого действия. Конечно, для сохранения объекта в базе данных дополнительный код не требуется, он здесь только в учебных целях:

[HttpPost]
public IActionResult Post([FromBody] Student student)
{
    if (student == null)
        return BadRequest();

    if (!ModelState.IsValid)
        return BadRequest();

    var stateBeforeAdd = _context.Entry(student).State;

    _context.Add(student);

    var stateAfterAdd = _context.Entry(student).State;

    _context.SaveChanges();

    var stateAfterSaveChanges = _context.Entry(student).State;

    return Created("URI of the created entity", student);
}

Как мы видим, перед добавлением нашей сущности в базу данных она находится в состоянии Detached. Как только мы используем метод Add, он принимает состояние Added. Наконец, после метода SaveChanges он имеет состояние Unchanges.

Использование метода AddRange

Когда мы хотим добавить несколько строк в базу данных, мы используем метод AddRange. Это та же процедура, только другой метод:

[HttpPost("postrange")]
public IActionResult PostRange([FromBody] IEnumerable<Student> students)
{
    //additional checks

    _context.AddRange(students);
    _context.SaveChanges();

    return Created("URI is going here", students);
}

Еще одна важная вещь, о которой следует упомянуть, заключается в том, что метод SaveChanges выполняет требуемую операцию, но также возвращает количество затронутых строк. Поэтому, если нам нужно проверить, сколько строк затронуто нашей операцией, мы всегда можем вернуть результат метода SaveChanges:

var result = _context.SaveChanges();

Добавление связанных объектов в базу данных

Теперь мы собираемся узнать, как включать отношения при добавлении основной сущности в базу данных. Чтобы продемонстрировать это, мы собираемся немного изменить наш первый пример:

[HttpPost]
public IActionResult Post([FromBody] Student student)
{
    //код валидации находится здесь

    student.StudentDetails = new StudentDetails
    {
        Address = "Added Address",
        AdditionalInformation = "Additional information added"
    };

    _context.Add(student);
    _context.SaveChanges();

    return Created("URI of the created entity", student);
}

Как видно из приведенного выше кода, мы только добавляем объект student к объекту контекста приложения, но объект studentDetails также добавляется в базу данных:

И мы можем проверить ответ от приложения:

Важно отметить, что мы не добавляем значение Id вручную для какой-либо сущности, за это отвечает EF Core. В базе данных видно, что значения GUID были созданы.

Отлично. То же самое можно сделать с отношениями "один ко многим" или "многие ко многим".

Обновление строк в базе данных

Есть два способа обновить строки в базе данных. С подключенным обновлением и с отключенным обновлением. Разница в том, что с подключенным обновлением мы используем один и тот же объект контекста для загрузки и изменения сущности. При отключенном обновлении дело обстоит иначе. Либо мы используем разные объекты контекста, либо получаем объект от клиента, который имеет все свойства как сущность в базе данных, поэтому мы можем обновить его без предварительной загрузки действия.

При работе с подключенным обновлением необходимо выполнить три основных шага:

  • Чтение данных из базы данных (с ее связями или без них).
  • Изменить одно или несколько свойств.
  • Вызов метода SaveChanges.

Итак, давайте посмотрим на связанное обновление в действии, когда мы отправим объект student от клиента с обновленным свойством Name:

[HttpPut("{id}")]
public IActionResult PUT (Guid id, [FromBody] Student student)
{
    var dbStudent = _context.Students
        .FirstOrDefault(s => s.Id.Equals(id));

    dbStudent.Age = student.Age;
    dbStudent.Name = student.Name;
    dbStudent.IsRegularStudent = student.IsRegularStudent;

    _context.SaveChanges();

    return NoContent();
}

Мы видим все три шага в приведенном выше коде. Мы загружаем объект студента на основе его идентификатора, затем сопоставляем свойства из обновленного студента клиента, который устанавливает состояние объекта на Modified и, наконец, мы вызываем SaveChanges. Несмотря на то, что мы изменили только свойство Name, мы должны сопоставить все свойства, потому что мы не знаем, что именно было изменено на стороне клиента.

EF Core, с другой стороны, содержит информацию о том, что именно было изменено.

Когда мы загружаем нашу сущность, EF Core начинает отслеживать ее, и в этот момент состояние становится Unchanged. Как только мы изменяем какое-либо свойство загруженного объекта, оно изменяет State на Modified. Наконец, после метода SaveChanges состояние возвращается к Unchanges.

Как только мы отправим запрос на это действие PUT, имя учащегося будет обновлено:

Мы видим, что обновлено только свойство Name. Если вы не видите обе команды в окне консоли, вы наверняка можете найти их в окне вывода в Visual Studio.

Свойство IsModified

Когда у нас есть сущность, которая уже находится в состоянии Modified, EF Core использует другое свойство, которое предоставляет информацию о том, что действительно изменилось. Это свойство IsModified, и мы можем изучить, как оно работает, немного изменив наш код:

[HttpPut("{id}")]
public IActionResult PUT (Guid id, [FromBody] Student student)
{
    var dbStudent = _context.Students
        .FirstOrDefault(s => s.Id.Equals(id));

    dbStudent.Age = student.Age;
    dbStudent.Name = student.Name;
    dbStudent.IsRegularStudent = student.IsRegularStudent;

    var isAgeModified = _context.Entry(dbStudent).Property("Age").IsModified;
    var isNameModified = _context.Entry(dbStudent).Property("Name").IsModified;
    var isIsRegularStudentModified = _context.Entry(dbStudent).Property("IsRegularStudent").IsModified;

    _context.SaveChanges();

    return NoContent()
}

Результат здесь не требует пояснений.

Обновление отношений в EF Core

Добавить отношения к операциям обновления в EF Core довольно просто. Мы можем прикрепить реляционную сущность к основной сущности, изменить ее, а все остальное EF Core сделает за нас, как только мы вызовем метод SaveChanges. Процедура такая же, как и для действий create.

Давайте посмотрим, как обновить отношения в EF Core:

[HttpPut("{id}/relationship")]
public IActionResult UpdateRelationship(Guid id, [FromBody] Student student)
{
    var dbStudent = _context.Students
        .Include(s => s.StudentDetails)
        .FirstOrDefault(s => s.Id.Equals(id));

    dbStudent.Age = student.Age;
    dbStudent.Name = student.Name;
    dbStudent.IsRegularStudent = student.IsRegularStudent;
    dbStudent.StudentDetails.AdditionalInformation = "Additional information updated";

    _context.SaveChanges();

    return NoContent();
}

Когда мы отправляем нашу форму запроса Postman, мы собираемся обновить сущность student и сущность studentDetails:

Тот же процесс применяется к другим типам отношений.

Важно знать, что когда мы обновляем нашу основную сущность, добавляя новую сущность отношения, EF Core создаст новую строку в таблице отношений и соединит ее с основной сущностью с помощью внешнего ключа:

[HttpPut("{id}/relationship")]
public IActionResult UpdateRelationhip(Guid id, [FromBody] Student student)
{
    var dbStudent = _context.Students
        .FirstOrDefault(s => s.Id.Equals(id));

    dbStudent.StudentDetails = new StudentDetails
    {
        Id = new Guid("e2a3c45d-d19a-4603-b983-7f63e2b86f14"),
        Address = "added address",
        AdditionalInformation = "Additional information for student 2"
    };

    _context.SaveChanges();

    return NoContent();
}

Поскольку теперь мы знаем, как обновлять отношения в EF Core, мы можем перейти к отключенному обновлению.

Неотслеживаемое обновление в EF Core

Есть несколько способов выполнить неотслеживаемое обновление, и мы собираемся показать вам два способа сделать это. В первом примере мы собираемся прикрепить объект, отправленный от клиента, изменить его состояние, а затем сохранить его в базе данных:

[HttpPut("disconnected")]
public IActionResult UpdateDisconnected([FromBody] Student student)
{
    _context.Students.Attach(student);
    _context.Entry(student).State = EntityState.Modified;

    _context.SaveChanges();

    return NoContent();
}

Объект student, отправленный от клиента, имеет в начале состояние Detached. После того, как мы воспользуемся методом Attach, объект изменит состояние на Unchanged.

Это также означает, что с этого момента EF Core начинает отслеживать объект. Теперь мы собираемся изменить состояние на Modified и сохранить его в базе данных.

Это объект, отправленный клиентом:

{
    "id": "DF75D335-A0EA-4798-8D8E-D7BF940F4D1D",
    "name": "Student updated 3",
    "age": "22",
    "isRegularStudent": false
}

Как видите, у него также есть свойство Id. Кроме того, мы изменили свойства Name и IsRegularStudent, но EF Core обновит весь объект в базе данных.

Другой способ сделать то же самое - использовать метод Update или UpdateRange, если у нас есть несколько объектов, готовых к обновлению. Итак, давайте отправим тот же объект только с измененным свойством IsRegularStudent на true:

[HttpPut("disconnected")]
public IActionResult UpdateDisconnected([FromBody] Student student)
{
    _context.Update(student);

    _context.SaveChanges();

    return NoContent();
}

Мы видим разницу. Метод Update установит отслеживаемую сущность, а также изменит ее состояние с Detached на Modified. Таким образом, нам не нужно прикреплять сущность и явно изменять ее состояние, потому что это делает за нас метод Update. Этот подход также обновит весь объект, даже если мы изменили только одно свойство:

Итак, мы идем.

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

Операция удаления в EF Core

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

Soft Delete в Entity Framework Core

При использовании метода мягкого удаления объект скрывается, а не удаляется. Это не необычный способ выполнения действий удаления, потому что во многих реальных проектах мы не хотим действительно удалять какие-либо строки, потому что они потребуются для некоторой статистики позже.

По сути, мы не удаляем объект, мы обновляем его, изменяя его свойство (мы собираемся назвать его "Удаленным") на true.

Чтобы показать, как работает мягкое удаление, нам нужно внести некоторые изменения в нашу текущую модель базы данных. Для начала изменим модель Student, добавив дополнительное свойство:

public bool Deleted { get; set; }

Теперь давайте изменим класс StudentConfiguration, добавив дополнительный метод для фильтрации всех удаленных учащихся из запросов:

builder.HasQueryFilter(s => !s.Deleted);

Метод HasQueryFilter будет включать только те сущности Student, для свойства Deleted которых установлено значение false. Или, другими словами, он проигнорирует всех учащихся, для которых для параметра Deleted установлено значение true.

Чтобы применить эти изменения к базе данных, мы собираемся создать и применить миграцию:

 

PM> Add-Migration AddedDeletedPropertyToStudent
PM> Update-Database

После этого переноса для всех строк в таблице Student столбец Deleted будет иметь нулевое значение (false).

Чтобы продолжить, давайте создадим дополнительное действие "Удалить" в контроллере и отправим запрос на удаление:

[HttpDelete("{id}")]
public IActionResult Delete(Guid id)
{
    var student = _context.Students
        .FirstOrDefault(s => s.Id.Equals(id));

    if (student == null)
        return BadRequest();

    student.Deleted = true;
    _context.SaveChanges();

    return NoContent();
}

Мягкое удаление состоит из трех этапов:

  • Загрузка
  • Редактирование
  • Удаление

Это результат в базе данных:

Загрузка данных с использованием QueryFilter и игнорирование QueryFilter

С помощью метода HasQueryFilter мы убедились, что «удаленные» объекты не будут включены в результат запроса. Посмотрим, работает ли это:

var studentsWithoudDeleted = _context.Students.ToList();

EF Core обычно возвращает всех студентов из таблицы Student. Но с реализованным методом HasQueryFilter результат другой:

Мы можем подтвердить, что «Student updated 3» отсутствует, поскольку это единственное свойство, у которого для свойства Deleted установлено значение true.

Конечно, если мы хотим игнорировать наш фильтр запроса, мы можем сделать это, применив к запросу IgnoreQueryFilter:

var studentsWithDeleted = _context.Students
    .IgnoreQueryFilters()
    .ToList();

Этот запрос будет включать «удаленную» сущность:

Отличная работа. Давайте продолжим обычное удаление.

Удаление отдельной сущности с помощью EF Core

При обычном удалении мы не изменяем нашу сущность, а фактически удаляем ее из базы данных с помощью метода Remove или RemoveRange для нескольких сущностей:

[HttpDelete("{id}")]
public IActionResult Delete(Guid id)
{
    var student = _context.Students
        .FirstOrDefault(s => s.Id.Equals(id));

    if (student == null)
        return BadRequest();

    _context.Remove(student);
    _context.SaveChanges();

    return NoContent();
}

Это действие также выполняется в три этапа:

  • Загрузка объекта "Student" и установка его состояния на "Без изменений".
  • Использование метода Remove для установки состояния "Удалено".
  • Сохранение изменений в базе данных

Мы должны быть осторожны с действиями удаления, если у нашей сущности есть связи, и мы не указали их в действии удаления. Удаление основного объекта может привести к удалению и взаимосвязей (каскадное удаление), в зависимости от конфигурации объекта.

Это переведенная команда SQL:

Мы видим, что первая инструкция Select включает фильтр запроса. Сразу после фильтра идет наш запрос на удаление.

Удаление объекта вместе со связанными данными

Чтобы удалить объект с его отношениями, все, что нам нужно сделать, это включить эту связь в основную сущность:

[HttpDelete("{id}/relationship")]
public IActionResult DeleteRelationships(Guid id)
{
    var student = _context.Students
        .Include(s => s.StudentDetails)
        .FirstOrDefault(s => s.Id.Equals(id));

    if (student == null)
        return BadRequest();

    _context.Remove(student);
    _context.SaveChanges();

    return NoContent();
}

И это все, что нам нужно сделать. Остальное EF Core сделает за нас.

Заключение

Мы рассмотрели множество тем, связанных с Entity Framework Core и его использованием в ASP.NET Core.

В этой серии статей мы узнали, как интегрировать EF Core в приложение ASP.NET Core и как настроить нереляционную и реляционную конфигурацию. Кроме того, мы научились использовать операции миграции, запросов и изменения базы данных.

Мы надеемся, что вам понравилось это читать и вы нашли много полезной информации.