Search code examples
c#nhibernatemany-to-manytransient

How do I properly update an NHibernate Many-to-Many relationship with Transient objects? TransientObjectException


I am getting a TransientObjectException when attempting to save an object that is part of a many-to-many association. I somewhat understand why this is happening, but want to understand how to properly accomplish what I am trying to do.

What I am trying to do, in a nutshell:

My application has a list of users and a list of roles. Users can be assigned to multiple roles, and roles can be assigned to multiple users. There is a web page where an admin can perform these assignments, and the assignments can be done in both directions (e.g., the admin can pick a user, then add roles to it; OR, pick a role and add users to it).

For example, let's say the admin clicks "Edit" on user "Alice." The admin is presented with a list of roles available and a list of roles already assigned to Alice. The admin then assigns a new role to Alice and clicks "Save."

On the server, the transient User and assigned Role objects are received from the client. If I just assign the transient Role list to the User object (e.g., user.Roles = roles), I can update just fine. However, if I happen to read the User from the database before making this assignment, I get a TransientObjectException on the associated Role object.

Class definitions:

public class Role
{
    public Guid ID { get; set; }
    public virtual IList<User> Users { get; set; }
}

public class User
{
    public Guid ID { get; set; }
    public virtual IList<Role> Roles { get; set; }
}

Mapping:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="Role">
    <id name="ID">
      <generator class="guid"/>
    </id>

    <bag name="Users" table="Role_User" lazy="false" cascade="none">
      <key column="RoleID" />
      <many-to-many column="UserID" class="User" />
    </bag>
  </class>
</hibernate-mapping>

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="User">
    <id name="ID">
      <generator class="guid"/>
    </id>

    <bag name="Roles" table="Role_User" lazy="false" cascade="none">
      <key column="UserID" />
      <many-to-many column="RoleID" class="Role" />
    </bag>
  </class>
</hibernate-mapping>

Code to save (works)

public void UpdateUser(User user, IList<Role> associatedRoles)
{
    using (var session = _sessionFactory.OpenSession())
    {
        user.Roles = associatedRoles;
        session.Merge<User>(user);
    }
}

Code to save (fails)

public void UpdateUser(User user, IList<Role> associatedRoles)
{
    using (var session = _sessionFactory.OpenSession())
    {
        User originalUser = session.Get<User>(user.ID);
        // Code that does some audit reporting/logging
        LogDifferences(originalUser, user);
        user.Roles = associatedRoles;
        session.Merge<User>(user);
    }
}

object is an unsaved transient instance - save the transient instance before merging


Solution

  • Miroslav Popovic's points are highly relevant; however, in my case I was running into problems because of entities in the database which had a Version set to an empty guid. These records were created outside of NHibernate. When saving an association between them, NHibernate was failing because it detected one of the association entries as a transient object due to its 0 version, even though it referred to an existing item.