views:

83

answers:

2

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);

    PagedList<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?

A: 

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.

ondesertverge
+2  A: 

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);

    // ...
}
Stefan Steinegger
is there a way of finding this in the session or how do I find it?
Rookian
Find what? The employee already in the session? It is most probably loaded by the team. *Are* you serializing entities or not?
Stefan Steinegger
I edited my question, please have a look.
Rookian
I'm quite sure that you load the employee with the team. Put the employee into the session before the query or use merge. Both should fix the problem.
Stefan Steinegger
I load the employee and the team with the ICriteria API via an outer join in a grid. But is this really the problem? Could you explain why your solution (first save/update the employee) works fine? I will update my post. Big thanks for now ;)
Rookian
I prefere your first solution. But I still do not understand why this works. Before I update the employee, I select all team Ids and the employee which I want to edit. So this means I have the employee in the session #1. Now a second session #2 is created, because a new web request has to be send for updating the employee. Now I add the team to employee (if he is not already added) and update the employee with the help of the session #2. Where is the problem :s ? Sry for being a kind of stupid :/
Rookian
Added another paragraph
Stefan Steinegger