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?
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With