Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid NHibernate.NonUniqueObjectException

Tags:

nhibernate

I'm writing a blog engine as a learning exercise. I know there are plenty of blog engines out there, but bear with me...

I have a BlogPost entity that has a property Tags that is an IList of tags associated with it. The BlogPost.SetTags(string) method splits the string, creates new Tag objects with the specified tag name, and adds them to the list. Same for BlogPost.AddTag(string tagName).

What I would like to have happen is that when I call BlogPost.AddTag("foo") where a tag entity named "foo" already exists and is persisted in the database, nHibernate just realizes that and wires up the post with the existing tag.

In the BlogRepository.Save() method, I check to see if each tag in the Tags list already exists. If not, I Save it with a call to TagRepository.Save(tag);

The problem is, in the sample code below, I'm getting an error "NHibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: tag 1, of entity: CMS.Core.Model.Tag" when I try to persist a BlogPost object using an existing tag. When I persist a BlogPost object that only uses new tags, they're created and everything is fine.

Note I'm also using the TagName as the primary key in the database for the bp_Tags table. It seemed superfluous to use an integer or GUID PK when the table only stores unique Tag names.

My nHibernate configuration looks like:

  <class name="CMS.Core.Model.Tag,CMS.Core" table="bp_Tags">
    <id column="TagName" name="TagName" type="String" unsaved-value="">
      <generator class="assigned" />
    </id>
  </class>

  <class name="CMS.Core.Model.BlogPost,CMS.Core" table="bp_Content">
    <id name="Id" column="Id" type="Int32" unsaved-value="0">
      <generator class="native"></generator>
    </id>
    <property name="SubmittedBy" column="SubmittedBy" type="string" length="256" not-null="true" />
    <property name="SubmittedDate" column="SubmittedDate" type="datetime" not-null="true" />
    <property name="PublishDate" column="PublishDate" type="datetime" not-null="true" />
    ...    
    <bag name="_tagsList" table="bp_Tags_Mappings" lazy="false" cascade="all">
      <key column="Target_Id" />
      <many-to-many class="CMS.Core.Model.Tag,CMS.Core" column="TagName" lazy="false" />
    </bag>

NHibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: tag 1, of entity: Bariliant.CMS.Core.Model.Tag

    BlogPost post, post2;

    using (UnitOfWork.Start())
    {  
        post = BlogPostFactory.CreateBlogPost("test post", "test body");
        post.Publish();
        BlogRepository.Save(post);
        UnitOfWork.Current.Flush();

        post.SetTags("tag 1, tag 2");
        BlogRepository.Save(post);
        UnitOfWork.Current.Flush();
    }

    using (UnitOfWork.Start())
    {
        post2 = BlogPostFactory.CreateBlogPost("test post2", "test body");
        post2.Publish();
        BlogRepository.Save(post2);
        UnitOfWork.Current.Flush();

        post2.AddTag("tag 1");
        BlogRepository.Save(post2);  // throws

...

Any thoughts on what I'm doing wrong and how to fix it?

like image 836
Jason Barile Avatar asked Nov 11 '09 05:11

Jason Barile


2 Answers

Since TagName is the ID, you are running up against NHibernate's identity map. Its identity map is already aware of an object with the same ID, so it's giving you that exception.

You might want to try something where you look to see if that Tag already exists in that session and if so, then associate that prexisting Tag with the 2nd post.

Psuedo-code example:

var tag = session.Get<Tag>("Tag 1");

if (tag != null)
{
   post.AddTag(tag);
}
else
{
   post.AddTag(new Tag("Tag 1"));
}

This blog posting will give you a detailed explanation: NHibernate - Cross session operations

like image 142
Daniel Auger Avatar answered Nov 30 '22 17:11

Daniel Auger


The way you're going about this isn't the way I would do it, but here's how to solve your problem. Normally in object oriented programming the following 2 objects are NOT equal:

var object1 = new Tag("hello");
var object2 = new Tag("hello");

var areSame = (object1 == object2); // false

You've made 2 seperate objects with identical state, but they are two different objects so if you compare them for equality then they are not the same. Clearly when it comes to NHibernate these objects are actually the same entity.

We solve this for NHibernate by overriding 2 methods of the Object Class. GetHashCode() and Equals()

GetHashCode() basically returns a unique hashcode based on an object's state. Equals() compares two objects for equality

like this:

public override int GetHashCode()
{
    return (this.GetType() + "|" + _tagName).GetHashCode();
}

public override bool Equals(object obj)
{
    return this.GetHashCode() == obj.GetHashCode();
}

Basically GetHashCode concatenates the object type and the name of the tag as a string ie App.Domain.Tag|nameoftag and generates a hashcode for that string

Equals() then compares the GetHashCode() result for the first object with the GetHashCode() result for the second object to test for equality. If you do this with the two objects we defined above, the two hashcodes will be the same, and therefore the comparison for Equals() will be true. When NHibernate tests the two objects for equality in its inner-workings it will determine they are the same and it should solve your problem.

like image 42
reach4thelasers Avatar answered Nov 30 '22 16:11

reach4thelasers