Reputation: 15324
My project has a Ticket
entity with an OwnedBy
property. I'm using nHibernate to persist the tickets to a database.
The canonical source for potential ticket owners is Active Directory. Since I don't want to have to query Active Directory every time I load tickets, I also persist Ticket.OwnedBy
to the database and load it from there when fetching tickets.
When a ticket's owner is reassigned, I get the new Owner
from Active Directory and assign it to Ticket.OwnedBy
, then call Session.SaveOrUpdate(ticket). When I commit the transaction, NHibernate throws a NonUniqueObjectException
because an Owner
with the same ID is already associated with the session.
class Ticket {
public int Id { get; set; }
public Owner OwnedBy { get; set; }
/* other properties, etc */
}
class Owner {
public Guid Guid { get; set; }
public string Name { get; set; }
public string Email { get; set; }
/* other properties, etc */
}
class TicketMap : ClassMap<Ticket> {
public TicketMap() {
Id(x => x.Id);
References(x => x.OwnedBy)
.Cascade.SaveUpdate()
.Not.Nullable();
/* other properties, etc */
}
}
class OwnerMap : ClassMap<Owner> {
public OwnerMap() {
Id(x => x.Guid)
.GeneratedBy.Assigned()
Map(x => x.Name);
Map(x => x.Email);
/* other properties, etc */
}
}
// unitOfWork.Session is an instance of NHibernate.ISession
Ticket ticket = unitOfWork.Session.Get<Ticket>(1);
Owner newOwner = activeDirectoryRepo.FindByGuid(/* guid of new owner, from user */);
ticket.OwnedBy = newOwner;
unitOfWork.Session.SaveOrUpdate(ticket);
unitOfWork.Commit(); // Throws NonUniqueObjectException
I want nHibernate to overwrite the properties of the existing Owner
with the properties of the unattached one. (The Name or Email in the object I fetched from AD may be different, and AD is supposed to be the canonical source.) I've tried calling Session.SaveOrUpdateCopy(ticket.OwnedBy)
and Session.Merge(ticket.OwnedBy)
before the SaveOrUpdate(ticket), but the exception is still being thrown. I've also read this related question about NonUniqueObjectException
, but calling Session.Lock() didn't work either.
I have two questions:
Upvotes: 2
Views: 1676
Reputation: 4221
Merge works, the most likely issue is you did not call it properly. Merge will update the existing object with the new object properties but Merge does not attach the new object. So you have to use the existing one. If you use the new object after a merge you still get the same error.
The following code should fix the problem:
//Merge the new Owner
unitOfWork.Session.Merge(newOwner);
//Get a valid Owner by retrieving from the session
Owner owner = session.Get<Owner>(newOwner.Id);
// Set the ticket to this owner instance instead of the new one
ticket.OwnedBy = owner;
unitOfWork.Session.Update(ticket);
unitOfWork.Commit();
The Owner retrieved from the session by Get will have the newOwner properties but also be valid for the session.
Upvotes: 2
Reputation: 3160
In order to persist the detached instance of an existing Owner-entity, it should be enough to call merge on the Owner instance without the call to SaveOrUpdate. It will then either insert the entity or update the existing one.
If merge does not work, then something is wrong. Post more code in that case and post your mapping.
BTW: Do you persist the Tickets, too? If so, the mapping seems rather odd. You should have a unique ID on Ticket and Map OwnedBy as a Reference, probably as an inverse mapping with a cascade on it.
Update: You should map it from both sides. Map the owner-side as a HasMany to Tickets with your Cascade and as Inverse Mapping. Map the Ticket side as Cascade.None() and as a Reference.
public TicketMap() {
Id(x => x.Id);
References(x => x.OwnedBy)
.Cascade.None()
.Not.Nullable();
/* other properties, etc */
}
class OwnerMap : ClassMap<Owner> {
public OwnerMap() {
Id(x => x.Guid)
.GeneratedBy.Assigned()
Map(x => x.Name);
Map(x => x.Email);
HasMany<Ticket>(x => x.Tickets).KeyColumn("TicketId").Cascade.AllDeleteOrphan().LazyLoad().Inverse().NotFound.Ignore();
/* other properties, etc */
}
That should work nicely.
Upvotes: 0