Cannot merge an object with a composite identifier if the object was previously retrieved using Get

The project I'm working on requires data synchronization in our system with another (another system is quite popular, so synchronization is so important). However, I have a strange problem when I try to update an existing object with a composite identifier.

The problem is that whenever the restored object is retrieved (using Get ) before the Merge call, it does not work (the changes are not saved to the DB, but the exception is not thrown). When I delete the Get call, updating the object works. Knowing whether an entity exists is necessary because if it is created, it is necessary to generate a part of a composite identifier.

bool exists = ScanForInstance(instance); using (var session = SessionFactoryFactory.GetSessionFactory<T>().OpenSession()) { if (exists) { instance = (T)session.Merge(instance); } else { KeyGenerator.Assign<T>(instance); newId = session.Save(instance); } session.Flush(); } 

The Get call is executed in ScanForInstance :

 private bool ScanForInstance<T>(T instance) where T : class { var id = IdResolver.ResolveObject<T>(instance); using (var session = SessionFactoryFactory.GetSessionFactory<T>().OpenStatelessSession()) { return session.Get<T>(id) != null; } } 

IdResolver is used to determine what should be used for the identifier (the value of one key in the mapping, otherwise the object itself for objects with composite identifiers).

As I said, if I delete the Get call, it works fine. It works great for all other operations (create, read and delete). All operations, including updating, work fine for objects with single keys.


Pervasive database and there are a certain number of restrictions:

  • No, I cannot change any scheme (I see this as a frequent answer to problems with FNB).
  • I do not want to simply delete and then insert, because there are some columns that we do not synchronize with our system, and I do not want to destroy them.

UPDATED: I added a simple example that people can copy / paste to test this strange behavior (if it is truly universal). I hope people do this to at least confirm my problem.

Type to display, Free mapping:

 public class ParentType { public virtual long AssignedId { get; set; } public virtual long? GeneratedId { get; set; } public virtual string SomeField { get; set; } public override bool Equals(object obj) { return Equals(obj as ParentType); } private bool Equals(ParentType other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; return AssignedId == other.AssignedId && GeneratedId == other.GeneratedId; } public override int GetHashCode() { unchecked { int hash = GetType().GetHashCode(); hash = (hash * 31) ^ AssignedId.GetHashCode(); hash = (hash * 31) ^ GeneratedId.GetHashCode(); return hash; } } } public class ParentMap : ClassMap<ParentType> { public ParentMap() { Table("STANDARDTASKITEM"); CompositeId() .KeyProperty(x => x.AssignedId, "STANDARDTASK") .KeyProperty(x => x.GeneratedId, "STANDARDTASKITEM"); Map(x => x.SomeField, "DESCRIPTION"); Not.LazyLoad(); } } 

Do not mind that it is called ParentType. Actually, I have no other mappings with this, and actually don't use this type as the parent type in this example. This is called that because I'm going to open up another question that is related to issues with compound identifiers and inheritance ( DO NOT USE COMPOSITE IDs !: -D ).

For actual testing, I just created a console project in VS with this as Program.cs :

 static void Main(string[] args) { var smFactory = Fluently.Configure() .Database(() => new OdbcPersistenceConfigurer() .Driver<OdbcDriver>() .Dialect<GenericDialect>() .Provider<DriverConnectionProvider>() .ConnectionString(BuildSMConnectionString()) .ProxyFactoryFactory(typeof(NHibernate.ByteCode.Castle.ProxyFactoryFactory)) .UseReflectionOptimizer() .UseOuterJoin()) .Mappings (m => m.FluentMappings.Add<ParentMap>() ); var sessionFactory = smFactory.BuildSessionFactory(); var updatedInstance = new ParentType { AssignedId = 1, GeneratedId = 13, SomeField = "UPDATED" }; bool exists; using (var session = sessionFactory.OpenStatelessSession()) { exists = session.Get<ParentType>(updatedInstance) != null; } using (var session = sessionFactory.OpenSession()) { if (exists) { session.Merge(updatedInstance); session.Flush(); } } } private static string BuildSMConnectionString() { // Return your connection string here } class OdbcPersistenceConfigurer : PersistenceConfiguration<OdbcPersistenceConfigurer, OdbcConnectionStringBuilder> { } 

I know that adding this example is a little more useful, since anyone who wants to test this will either have to change the ParentType field to fit the table that they already have in their own database, or add a table to match what is displayed in ParentType. I hope that someone will do this at least out of curiosity, now that I have a good start in testing.

+4
source share
1 answer

Well, at least I figured out a solution to my problem, but not why. My solution was to create a new type that covered the properties that I used as the composite id:

 public class CompositeIdType { public virtual long AssignedId { get; set; } public virtual long GeneratedId { get; set; } public override bool Equals(object obj) { return Equals(obj as CompositeIdType); } private bool Equals(CompositeIdType other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; return AssignedId == other.AssignedId && GeneratedId == other.GeneratedId; } public override int GetHashCode() { unchecked { int hash = GetType().GetHashCode(); hash = (hash * 31) ^ AssignedId.GetHashCode(); hash = (hash * 31) ^ GeneratedId.GetHashCode(); return hash; } } } 

Then replace the properties in ParentType to reference this new type:

 public class ParentType { public virtual CompositeIdType Key { get; set; } public virtual string SomeField { get; set; } } 

With these changes, the new mapping will be:

 public class ParentMap : ClassMap<ParentType> { public ParentMap() { Table("STANDARDTASKITEM"); CompositeId<CompositeIdType>(x => x.Key) .KeyProperty(x => x.AssignedId, "STANDARDTASK") .KeyProperty(x => x.GeneratedId, "STANDARDTASKITEM"); Map(x => x.SomeField, "DESCRIPTION"); Not.LazyLoad(); } } 

After all these changes are completed, Merge works even when you call Get before the call Merge . My best bet is not the general form. CompositeId doesn’t do something right or that the generated mapping does not work well with NH when you call Merge on an entity that uses it (I would like to go to the FNH source to fix this, if so, but I have already spent too much time figuring out how to get around this problem).

This is good and good, but it will require me to create a new type for each object that I collect, or at least a new type for an identifier with a different number of keys (i.e. a type with 2 keys, a type with 3 keys and etc.).

To avoid this, I can hack it so that you add a link of the same type that you are matching, and set the link to this in the constructor:

 public class ParentType { public ParentType() { Key = this; } public virtual ParentType Key { get; set; } public virtual long AssignedId { get; set; } public virtual long GeneratedId { get; set; } public virtual string SomeField { get; set; } public override bool Equals(object obj) { return Equals(obj as ParentType); } private bool Equals(ParentType other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; return AssignedId == other.AssignedId && GeneratedId == other.GeneratedId; } public override int GetHashCode() { unchecked { int hash = GetType().GetHashCode(); hash = (hash * 31) ^ AssignedId.GetHashCode(); hash = (hash * 31) ^ GeneratedId.GetHashCode(); return hash; } } } 

Then the mapping will be:

 public class ParentMap : ClassMap<ParentType> { public ParentMap() { Table("STANDARDTASKITEM"); CompositeId<ParentType>(x => x.Key) .KeyProperty(x => x.AssignedId, "STANDARDTASK") .KeyProperty(x => x.GeneratedId, "STANDARDTASKITEM"); Map(x => x.SomeField, "DESCRIPTION"); Not.LazyLoad(); } } 

I tested this for updates and inserts using Combine with Receive , received the call before the merge, and unexpectedly IT WORKS . I'm still on a fence that has a fix (a new type encompassing a composite identifier or self-promotion), as self-esteem seems a bit hacked for my tastes.

If anyone finds out WHY this didn't work initially, I would still like to know ...

+3
source

All Articles