Brant
Brant

Reputation: 15324

Can I force nHibernate to persist a new entity with the same ID as an already-loaded entity?

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 definitions

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 */
}

Fluent nHibernate Mappings

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 */
    }
}

Sample code

// 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:

  1. Is there an easy way to bend nHibernate to my will?
  2. I may have made an architectural misstep in trying to treat the owners I fetch from AD as the same type as the owners I store in the DB. How can I improve this design so I won't need to bend nHibernate to my will?

Upvotes: 2

Views: 1676

Answers (2)

Sisyphus
Sisyphus

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

Falcon
Falcon

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

Related Questions