开发者

NHibernate - How to handle the NonUniqueObjectException?

I get this error:

a different object with the same identifier value was already associated with the session: 63, of entity: Core.Domain.Model.Employee

Within my ASP.NET MVC controller actions I do this:

public ActionResult Index(int? page)
{
    int totalCount;
    Employee[] empData = _employeeRepository.GetPagedEmployees(page ?? 1, 5, out totalCount);

    EmployeeForm[] data = (EmployeeForm[]) Mapper<Employee[], EmployeeForm[]>(empData);

    Paged开发者_JAVA百科List<EmployeeForm> list = new PagedList<EmployeeForm>(data, page ?? 1, 5, totalCount);


    if (Request.IsAjaxRequest())
        return View("Grid", list);

    return View(list);
}

public ActionResult Edit(int id)
{
    ViewData[Keys.Teams] = MvcExtensions.CreateSelectList(
        _teamRepository.GetAll().ToList(), 
        teamVal => teamVal.Id, 
        teamText => teamText.Name);
    return View(_employeeRepository.GetById(id) ?? new Employee());
}

[HttpPost]
public ActionResult Edit(
    Employee employee, 
    [Optional, DefaultParameterValue(0)] int teamId)
{
    try
    {
        var team = _teamRepository.GetById(teamId);
        if (team != null)
        {
            employee.AddTeam(team);
        }

        _employeeRepository.SaveOrUpdate(employee);

        return Request.IsAjaxRequest() ? Index(1) : RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}

Mapping files:

Employee

public sealed class EmployeeMap : ClassMap<Employee>
{
    public EmployeeMap()
    {
        Id(p => p.Id)
            .Column("EmployeeId")
            .GeneratedBy.Identity();

        Map(p => p.EMail);
        Map(p => p.LastName);
        Map(p => p.FirstName);

        HasManyToMany(p => p.GetTeams())
            .Access.CamelCaseField(Prefix.Underscore)
            .Table("TeamEmployee")
            .ParentKeyColumn("EmployeeId")
            .ChildKeyColumn("TeamId")
            .LazyLoad()
            .AsSet()
            .Cascade.SaveUpdate();

        HasMany(p => p.GetLoanedItems())
            .Access.CamelCaseField(Prefix.Underscore)
            .Cascade.SaveUpdate()
            .KeyColumn("EmployeeId");
    }
}

Team:

public sealed class TeamMap : ClassMap<Team>
{
    public TeamMap()
    {
        Id(p => p.Id)
            .Column("TeamId")
            .GeneratedBy.Identity();

        Map(p => p.Name);

        HasManyToMany(p => p.GetEmployees())
            .Access.CamelCaseField(Prefix.Underscore)
            .Table("TeamEmployee")
            .ParentKeyColumn("TeamId")
            .ChildKeyColumn("EmployeeId")
            .LazyLoad()
            .AsSet()
            .Inverse()
            .Cascade.SaveUpdate();
    }
}

How do I fix this bug and what is the problem here?


You are most probably adding two employees with the same id to the session.

Find the reason why there are two employees with the same id.

Are you using detached (e.g. serialized) entities? You most probably have the entity loaded from the database (eg. when loading the team) and another which is detached and gets added. The best you can do is not using the detached instance at all. Only use its id to load the real instance from the database:

var employee = employeeRepository.GetById(detachedEmployee.id);
var team = _teamRepository.GetById(teamId);
if (team != null)
{
    employee.AddTeam(team);
}
_employeeRepository.SaveOrUpdate(employee);

When you want to write the values in the detachedEmployee over the employee in the session, use session.Merge instead of session.Update (or session.SaveOrUpdate):

var employee = employeeRepository.Merge(detachedEmployee);
// ...

There are other reasons for this error, but I don't know what you are doing and how your mapping file looks like.


Edit

This should work:

// put the employee into the session before any query.
_employeeRepository.SaveOrUpdate(employee);

var team = _teamRepository.GetById(teamId);
if (team != null)
{
    employee.AddTeam(team);
}

Or use merge:

var team = _teamRepository.GetById(teamId);
if (team != null)
{
    employee.AddTeam(team);
}
// use merge, because there is already an instance of the
// employee loaded in the session.
// note the return value of merge: it returns the instance in the
// session (but take the value from the given instance)
employee = _employeeRepository.Merge(employee);

Explanation:

There could only be one instance of the same database "record" in memory. NH ensures that the same instance is returned by a query as already is in the session cache. If it wouldn't be like this, there would be more than one value in memory for the same database field. This would be inconsistent.

Entities are identified by their primary key. So there must be only one instance per primary key value. Instances are put into the session by queries, Save, Update, SaveOrUpdate or Lock.

You get a problem when an instance is already in the session (eg. by a query) and you try to put another instance (eg. a detached / serialized one) with the same id into the session (eg. using update).

Solution one puts the instance into the session before any other query. NH will return exactly this instance in all subsequent queries! This is really nice, but you need to call Update before any query to make it work properly.

Solution two uses Merge. Merge does the following:

  • if the instance is already in the cache, it writes all the properties from the argument into the one in the cache and returns the one in the cache.
  • is the instance not in the cache, it retrieves it from the database (which requires a query). Then it adds the argument into the cache. It returns the also the instance in the cache which is the same as the argument. In contrast to a simple Update, it does not blindly update the values in the database, it retrieves it it first. This allows NH to perform either an insert or update, omit the update at all (because it is unchanged) and some more special cases.

At the end, Merge does never have a problem with already existing instances. It does not even matter if the instance is already in the database or not. You just need to take into account that the return value is the instance in the session, not the argument.


Edit: the second Employee instance

[HttpPost]
public ActionResult Edit(
    Employee employee, // <<== serialized (detached) instance
    [Optional, DefaultParameterValue(0)] int teamId)
{
    // ...

    // load team and containing Employees into the session
    var team = _teamRepository.GetById(teamId);


    // ...

    // store the detached employee. The employee may already be in the session,
    // loaded by the team query above. The detached employee is another instance
    // of an already loaded one. This is not allowed.
    _employeeRepository.SaveOrUpdate(employee);

    // ...
}


My guess is that you have a Team.Employee that is loaded differently than the employee in your code. Try setting your team.Employee to the one here.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜