Как обрабатывать обновления объектов. NHibernate + ASP.NET MVC
Я не могу обновить созданный ранее объект. Я получаю исключение StaleObjectException
с сообщением:
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [Project.DomainLayer.Entities.Employee#00000000-0000-0000-0000-000000000000]
Я не передаю процесс обновления никому. В чем проблема?
Доступ к данным /DI
public class DataAccessModule : Ninject.Modules.NinjectModule
{
public override void Load()
{
this.Bind<ISessionFactory>()
.ToMethod(c => new Configuration().Configure().BuildSessionFactory())
.InSingletonScope();
this.Bind<ISession>()
.ToMethod(ctx => ctx.Kernel.TryGet<ISessionFactory>().OpenSession())
.InRequestScope();
this.Bind(typeof(IRepository<>)).To(typeof(Repository<>))
.InRequestScope();
}
}
Доступ к данным/сопоставления
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Project.DomainLayer" namespace="Project.DomainLayer.Entities">
<class name="Employee" optimistic-lock="version">
<id name="ID" column="EmployeeID" unsaved-value="00000000-0000-0000-0000-000000000000">
<generator class="guid.comb" />
</id>
<version name="Version" type="Int32" column="Version" />
<!-- properties -->
<property name="EmployeeNumber" />
<!-- ... -->
<property name="PassportRegistredOn" not-null="true" />
<!-- sets -->
<set name="AttachedInformation" cascade="all">
<key column="EmployeeID" />
<element column="Attachment" />
</set>
<set name="TravelVouchers" cascade="all">
<key column="EmployeeID" />
<one-to-many class="TravelVoucher" />
</set>
</class>
</hibernate-mapping>
Доступ к данным/репозиторий
public class Repository<T> : IRepository<T> where T : AbstractEntity<T>, IAggregateRoot
{
private ISession session;
public Repository(ISession session)
{
this.session = session;
}
// other methods are omitted
public void Update(T entity)
{
using(var transaction = this.session.BeginTransaction())
{
this.session.Update(entity);
transaction.Commit();
}
}
public void Update(Guid id)
{
using(var transaction = this.session.BeginTransaction())
{
this.session.Update(this.session.Load<T>(id));
transaction.Commit();
}
}
}
Внутри контроллера
public class EmployeeController : Controller
{
private IRepository<Employee> repository;
public EmployeeController(IRepository<Employee> repository)
{
this.repository = repository;
}
public ActionResult Edit(Guid id)
{
var e = repository.Load(id);
return View(e);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Employee employee)
{
if(ModelState.IsValid)
{
repository.Update(employee);
return RedirectToAction("Deatils", "Employee", new { id = employee.ID });
}
else
{
return View(employee);
}
}
}
Как обновить объекты?
Спасибо!
ИЗМЕНИТЬ
Итак, я добавил unsaved-value="{Guid.Empty goes here}"
в свою разметку. Более того, я попытался сделать следующее:
public void Update(T entity)
{
using(var transaction = this.session.BeginTransaction())
{
try
{
this.session.Update(entity);
transaction.Commit();
}
catch(StaleObjectStateException ex)
{
try
{
session.Merge(entity);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}
И это дает мне тот же эффект. Я имею в виду transaction.Commit();
после того, как Merge
дает то же исключение.
Также мне интересно, следует ли открывать, используя скрытый ввод, объект ID
в представлении Edit
?
ИЗМЕНИТЬ
Таким образом, сущность действительно отделяется. Когда он переходит к контроллеру, ID
равен Guid.Empty
. Как мне обрабатывать, Merge
или Reattach
?
Ответы
Ответ 1
Есть два сценария, с которыми вы можете столкнуться, учитывая ваш шаблон кода.
1) Вы можете получить объект из db, используя ISession.Get, за которым может последовать изменение/обновление получаемого объекта. Чтобы это изменение было эффективным, все, что вам нужно сделать, это очистить сеанс или зафиксировать транзакцию, поскольку Nhibernate автоматически отслеживает все изменения.
2) У вас есть временный экземпляр, объект, который не связан с ISession в контексте, из которого вы хотите обновить. В этом случае, по моему опыту, наилучшей практикой является ISession. Получите объект и сделайте соответствующие изменения для объекта, который вы только что извлекли (обычно ваша модель просмотра отличается от вашей модели домена, не смешивайте оба ) Этот шаблон показан ниже. Он работает все время. Убедитесь, что вы также используете ISession.SaveOrUpdate.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Employee employee)
{
if(ModelState.IsValid)
{
var persistentEmployee = repository.Get(employee.Id);
if( persistentEmployee == null){
throw new Exception(String.Format("Employee with Id: {0} does not exist.", employee.Id));
}
persistentEmployee.Name = employee.Name;
persistentEmployee.PhoneNumber = employee.PhoneNumber;
//and so on
repository.Update(persistentEmployee);
return RedirectToAction("Deatils", "Employee", new { id = employee.ID });
}
else
{
return View(employee);
}
}
Также обратите внимание, что ваш контроллер, вероятно, создается на основе запроса, следовательно, срок службы вашего ISession не охватывает несколько вызовов для разных методов, которые вы используете в своем контроллере. Другими словами, каждый метод почти всегда работает в контексте нового ISession (единицы работы).
Ответ 2
Ваша логика не очень хорошая, потому что вы используете модель домена, например Employee, как ViewModel. Лучшей практикой является использование CreateEmploeeViewModel и EditEmployeeViewModel и отдельная логика логики и просмотра модели.
Например:
public class Employee
{
public virtual int Id { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string MiddleName { get; set; }
}
public class CreateEmployeeViewModel
{
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual string MiddleName { get; set; }
}
public class EditEmployeeViewModel : CreateEmployeeViewModel
{
public virtual int Id { get; set; }
}
Чтобы преобразовать из Employee в ViewModel, я предпочитаю использовать Automapper.
Итак, действия контроллера выглядят следующим образом:
[HttpGet]
public virtual ActionResult Edit(int id)
{
Employee entity = GetEntityById(id);
EmployeeEditViewModel model = new EmployeeEditViewModel();
Mapper.Map(source, destination);
return View("Edit", model);
}
[HttpPost]
public virtual ActionResult Edit(EmployeeEditViewModel model)
{
if (ModelState.IsValid)
{
Employee entity = GetEntityById(model.Id);
entity = Mapper.Map(model, entity);
EntitiesRepository.Save(entity);
return GetIndexViewActionFromEdit(model);
}
return View("Edit", model);
}
В этом случае NHibernate знает, что вы обновляете Employee, и вы не можете удалить некоторые свойства, которые не существуют в вашем представлении.
Ответ 3
Я считаю, что ваш объект Employee стал тем, что NHibernate называет "отключенным" между GET и POST ваших методов действий "Редактировать". Подробнее см. NHibernate на эту тему для получения более подробной информации и некоторых решений. Фактически, ссылка описывает точный сценарий GET-POST, который вы, кажется, используете.
Вам может понадобиться повторно привязать ваш объект Employee и/или указать "несохраненное значение", как предлагалось Firo, чтобы NHibernate знал сотрудника с идентификатором Guid.Empty еще не сохранялся в базе данных. В противном случае, как предположил Фиро, NHibernate видит Guid.Empty в качестве действительного идентификатора и считает, что объект уже сохранен в базе данных, но сеанс, в котором он был извлечен, был отброшен (следовательно, объект становится "отсоединенным" ).
Надеюсь, что это поможет.
Ответ 4
Вы спрашиваете,
Также мне интересно, следует ли открывать, используя скрытый ввод, идентификатор объекта в представлении "Изменить"?
Да, вам нужно. Вы также должны открыть Версия в скрытом вводе, поскольку его бизнес должен помочь предотвратить одновременные изменения для одного и того же объекта. Подсказки StaleObjectException, на которые у вас включено управление версиями, в этом случае обновление будет работать только в том случае, если возвращаемое вами значение версии (Int32) идентично таковой в базе данных.
Вы всегда можете обойти это, перезагрузив объект и скопировав его, гарантируя, что значение версии, вероятно, будет соответствовать, но это, кажется, подорвет его цель.
ИМХО, я поместил идентификатор и версию сущности в скрытый ввод, а после обратной передачи перезагрузите объект и сопоставьте данные. Таким образом, как сказал Иван Корытин, вам не нужно было носить с собой свойства, которые вам не нужны. Вы также можете справиться с неторопливостью на уровне контроллера и добавить ошибку проверки, а не NHibernate сказать, что ваш объект устарел.
Иван Корытин описывает стандартный процесс обработки простого редактирования объекта. Единственный вопрос с его ответом в том, что он не касается свойства Version. IMHO, база данных не должна быть версией, или должно иметь значение свойство Version.
Ответ 5
"несохраненное значение" отсутствует. поэтому NH считает, что Guid.Empty является действительным id
<id name="ID" column="EmployeeID" unsaved-value="0000000-0000-0000-0000-000000000000">
Ответ 6
Если вы хотите обновить некоторые поля объектов, вам не нужно использовать session.Update(),
используйте session.Flush() перед закрытой транзакцией.
session.Update() → Обновить постоянный экземпляр с идентификатором данного временного экземпляра.
Ответ 7
Если вы один из нас, от которого не помог ответ, попробуйте посмотреть, для чего отправляется идентификатор в вашей сущности.
У меня та же проблема, но в конце я увидел, что я меняю идентификатор на другой номер (в NHibernate идентификатор будет создан сам, , если вы настроите его таким образом!)..
Итак, в нижней части строки, проверьте, соответствует ли структура данных, которые вы отправляете, и значениям, что вы ожидаете отправить.
Надеюсь, я могу помочь кому угодно!:)
Ответ 8
В конце концов, это помогает, но я считаю это ужасным:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Guid id, Employee employee)
{
if(ModelState.IsValid)
{
var e = repository.Get(id);
if(Guid.Empty != e.ID)
{
e.Department = employee.Department;
repository.Update(employee.ID);
return RedirectToAction("Details", "Employee", new { id = e.ID });
}
/*...*/
}
}
Даже если я поместил поля HiddenFor
в представление Edit
для ID
(и Version
), передаваемый идентификатор является обычным a Guid.Empty
, в котором указано, что employee
является переходным.
Я очень благодарен за вашу помощь, ребята!
Вопросы
I know what viewmodels are, but quite not understood how does it help with detaching
.
Why if I put TextBoxFor(e => e.ID) on Edit view it binds employee like a transient entity without saving the ID value?