Реализация базовой CRUD-функциональности с Entity Framework в приложении ASP.NET MVC
В предыдущем уроке мы создали MVC-приложение, которое умеет хранить и показывать данные с использованием Entity Framework и SQL Server Compact. В этом уроке мы рассмотрим создание и настройку CRUD (create, read, update, delete)-функциональности, которую MVC scaffolding автоматически создает для вас в контроллерах и представлениях.
Note общепринятой практикой является реализация паттерна «репозиторий» для создания слоя абстракции между контроллером и слоем доступа к данным. Но это будет потом, в поздних уроках (Implementing the Repository and Unit of Work Patterns).
В этом уроке будут созданы следующие страницы:
Создание страницы Details
На странице Details будет показываться содержимое коллекции Enrollements в HTML-таблице.
В Controllers \ StudentController . csметод для представления Details представляет собой следующий код:
public ViewResult Details(int id)
{
Student student = db.Students.Find(id);
return View(student);
}
Метод Find используется для извлечения одной сущности Student, соответствующей переданному в метод параметру id. Значение id берется из строки запроса находящейся на странице Details ссылки.
Откройте Views \ Student \ Details . cshtml. Каждое поле отображается хелпером DisplayFor:
<div class="display-label">LastName</div>
<div class="display-field">
@Html.DisplayFor(model => model.LastName)
</div>
Для отображения списка enrollments добавьте следующий код после поля EnrollmentDate, до закрывающего тега fieldset:
<div class="display-label">
@Html.LabelFor(model => model.Enrollments)
</div>
<div class="display-field">
<table>
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</div>
Этот код проходит циклом через сущности в Enrollments navigation property. Для каждой сущности Enrollment он отображает название курса и grade. Название курса берётся из сущности Course , содержащейся в свойстве Course navigation property сущности Enrollments. Все эти данные автоматически выгружаются из базы данных по мере необходимости (иными словами, используется ленивая загрузка. Вы не указали eagerloading для свойства Courses navigation property, поэтому при первом обращении к этому свойству, будет выполнен запрос к базе для получения данных. Прочитать больше lazy loading и eager loading можно здесь Reading Related Data.)
Нажмите на вкладку Students и щелкните на ссылку Details.
Создание страницы Create
В Controllers\StudentController.cs, замените код метода HttpPost Create:
[HttpPost]
public ActionResult Create(Student student)
{
try
{
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
Таким образом мы добавляем сущность Student , созданную ASP.NET MVC Model Binder в соответствующее множество сущностей и затем сохраняем изменения в базу. (Modelbinder – функциональность ASP.NET MVC, облегчающая работа с данными, пришедшими из формы. Model binder конвертирует данные из формы в соответствующие типы данных .NET Framework и передает в нужный метод как параметры. В данном случае, model binder инстанциирует сущность Student используя значения свойства из коллекции Form.)
Блок try-catch является единственным различием нашего варианта и того, что было автоматически создано. Если исключение, наследованное от DataException , перехватывается во время сохранения изменений, выводится стандартное сообщение об ошибке. Такие ошибки обычно вызываются чем-то внешним нежели программистской ошибкой, поэтому пользователю просто предлагается попробовать ещё раз. Код в Views \ Student \ Create . cshtml похож на код из Details . cshtml за исключением EditorFor и ValidationMessageFor , используемых для каждого поля вместо хелпера DisplayFor. Следующий код приведен для примера:
<div class="editor-label">
@Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.LastName)
@Html.ValidationMessageFor(model => model.LastName)
</div>
В Create . cshtmlвносить изменения нет необходимости.
Нажмите на вкладку Students и на CreateNew.
Проверка данных включена по умолчанию. Введите имена и какую-нибудь неправильную дату и нажмите Create чтобы увидеть ошибку.
В этом случае вы видите проверку данных на клиентской стороне, реализованную с помощью JavaScript. Проверка данных на стороне сервера также реализована и, даже если проверка данных на стороне клиента пропустит плохие данные, они будут перехвачены на стороне сервера и будет выброшено исключение.
Измените дату на правильную, например, на 9/1/2005 и нажмите Create чтобы увидеть нового студента на странице Index.
Создание страницы Edit
В Controllers\StudentController.cs метод HttpGet Edit (тот, который без атрибута HttpPost) использует метод Find для извлечения выбранной сущности Student. Необходимости изменять код этого метода нет.
Замените код метода HttpPost Edit на следующий код:
[HttpPost]
public ActionResult Edit(Student student)
{
try
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
Код похож на то, что было в методе HttpPost Create, однако вместо добавления сущности в множество этот код устанавливает свойство сущности, определяющее, было ли оно изменено. При вызове SaveChanges свойство Modified указывает Entity Framework на необходимость создания SQL запроса для обновления записи в базе. Все столбцы записи будут обновлены, включая те, которые пользователь не трогал. Вопросы параллелизма игнорируются. (о вопросах параллелизма можно почитать в Handling Concurrency.)
Состояния сущностей и методы Attach и SaveChanges Methods
Контекст базы данных следит за синхронизацией сущностей в памяти с соответствующими записями в базе, и эта информация определяет то, что происходит при вызове метода SaveChanges. Например, при передаче новой сущности в метод Add, состояние этой сущности меняется на Added. Затем, при вызове метода SaveChanges, контекст базы данных инициирует выполнение SQL-запроса INSERT.
Состояние сущности может быть определено как:
- Added. Сущности еще нет в базе. Метод SaveChanges инициирует выполнение запроса INSERT.
- Unchanged. При вызове SaveChanges ничего не происходит. Данное состояние у сущности при извлечении её из базы данных.
- Modified. Значения свойств сущности были изменены, SaveChanges выполняет запрос UPDATE.
- Deleted. Сущность помечена к удалению, SaveChanges выполняет запрос DELETE.
- Detached. Состояние сущности не отслеживается контекстом базы данных.
В приложении для десктопа состояние меняется автоматически. В данном типе приложений вы извлекаете сущность и изменяете значения каких-либо свойств, что приводит к изменению состояния на Modified. После вызова SaveChanges Entity Framework генерирует SQL-запрос UPDATE , обновляющий только те свойства, значения которых были изменены.
Однако в веб-приложении алгоритм нарушается, так как экземпляр контекста базы данных, извлекающий сущность, уничтожается после перезагрузки страницы. При HttpPost Edit происходит новый запрос и у вас появляется новый экземпляр контекста, поэтому необходимо вручную менять состояние сущности на Modified. После этого, при вызове SaveChanges Entity Framework обновит все столбцы записи в базе, так как контекст уже не знает о том, какие свойства конкретно были изменены.
Если вы хотите, чтобы Update изменял только отредактированные пользователем поля, вы можете каким-либо образом сохранить оригинальные значения (например, скрытыми полями формы), сделав их доступными в момент вызова HttpPost Edit. Таким образом вы сможете создать сущность Student используя оригинальные значения, вызвать метод Attach с оригинальной версией сущности, обновить значения сущности и вызвать SaveChanges. За более подробной информацией можно обратиться к материалам Add/Attach and Entity States и посту в блоге команды разработки Entity Framework Local Data.
Код в Views \ Student \ Edit . cshtml аналогичен коду в Create . cshtml, вносить изменения не надо.
Нажмите на вкладке Students и затем на ссылку Edit.
Измените значения и нажмите Save.
Создание страницы Delete
В Controllers\StudentController.cs метод HttpGet Delete использует метод Find для извлечения выбранной сущности Student, также, как и в Details и Edit ранее. Для реализации своего сообщения об ошибке при ошибке вызова SaveChanges, необходимо добавить дополнительную функциональность к методу и соответствующему представлению.
Как и в случае с операциями обновления и создания, операция удаления также нуждается в двух методах. Метод, вызываемый в ответ на GET-запрос, показывает пользователю представлению, позволяющее подтвердить или отменить удаление. Если пользователь подтверждает удаление, создаётся POST-запрос и вызывается метод HttpPost Delete.
Вам необходимо добавить блок исключений try-catch в код метода HttpPost Delete для обработки ошибок, которые могут возникнуть при обновлении базы данных. Если возникает ошибка, метод HttpPost Delete вызывает метод HttpGet Delete, передавая ему параметр, сигнализирующий об ошибке. Метод HttpGet Delete снова генерирует страницу подтверждения удаления и текстом ошибки.
Замените код метода HttpGet Delete на следующий код, позволяющий обработать ошибки:
public ActionResult Delete(int id, bool? saveChangesError)
{
if (saveChangesError.GetValueOrDefault())
{
ViewBag.ErrorMessage = "Unable to save changes. Try again, and if the problem persists see your system administrator.";
}
return View(db.Students.Find(id));
}
Этот код принимает опциональный параметр булевого типа, сигнализирующий о возникновении ошибки. Этот параметр равен null (false) после вызова HttpGet Delete и true при вызове HttpPost Delete.
Замените код метода HttpPost Delete (DeleteConfirmed) следующим кодом, совершающим удаление и обрабатывающим ошибки:
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
try
{
Student student = db.Students.Find(id);
db.Students.Remove(student);
db.SaveChanges();
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
return RedirectToAction("Delete",
new System.Web.Routing.RouteValueDictionary {
{ "id", id },
{ "saveChangesError", true } });
}
return RedirectToAction("Index");
}
Код возвращает выбранную сущность и вызывает метод Remove, меняющий состояние сущности на Deleted. При вызове SaveChanges генерируется SQL-запрос DELETE.
Если производительность – приоритет, то можно обойтись без ненужных SQL-запросов, возвращающих запись путём замены кода, вызывающего методы Find и Remove:
Student studentToDelete = new Student() { StudentID = id };
db.Entry(studentToDelete).State = EntityState.Deleted;
Этим кодом мы инстанцируем сущность Student используя только значение первичного ключа и затем определяем состояние сущности как Deleted. Это всё, что нужно Entity Framework для удаление сущности.
Как уже упоминалось, метод HttpGet Delete не удаляет данные. Удаление данных в ответ на GET-запрос (или, также, редактирование, создание и любое другое действие с данными) создаёт брешь в безопасности. Для подробной информации смотрите ASP.NET MVC Tip #46 — Don't use Delete Links because they create Security Holes в блоге Stephen Walther.
В Views \ Student \ Delete . cshtmlдобавьте следующий код между h2 и h3:
<p class="error">@ViewBag.ErrorMessage</p>
Нажмите на вкладку Students и затем на ссылку Delete:
Нажмите Delete. Загрузится страница Index, уже без студента, которого мы удалили (вы увидите пример обработки кода в методе в уроке Handling Concurrency.)
Убеждаемся, что не осталось открытых подключений к базе данных
Чтобы быть уверенным, что все подключения к базе данных были правильно закрыты и ресурсы, занятые ими, освобождены, необходимо убедиться в том, что контекст уничтожается. Поэтому вы можете найти метод Dispose в конце класса контроллера StudentController в StudentController . cs:
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
Базовый класс Controller реализует интерфейс IDisposable, поэтому этот код просто переопределяет (override) метод Dispose(bool) для внешнего удаления экземпляра контекста.
Теперь у вас есть всё, что реализует простые CRUD операции для сущностей Student. В следующем уроке мы расширим функциональность страницы Index, добавив сортировку и разбивку на страницы.
Это перевод оригинальной статьи Implementing Basic CRUD Functionality with the Entity Framework in ASP.NET MVC Application. Благодарим за помощь в переводе Александра Белоцерковского.