Cannot Merge an entity with a Composite Id when th

2019-02-24 08:48发布

The project I am working on requires data in our system to be synchronized with that of another (the other system is quite popular which is why synchronization is so important). However, I am having a weird issue when I attempt to update an existing entity that has a composite id.

The problem is that whenever the entity to be updated is retrieved (using Get) prior to calling Merge, it does not work (the changes are not persisted to the DB but no exception is thrown). When I remove the call to Get, updating the entity works. Knowledge on whether the entity exists is needed because if it is being created, part of the composite id needs to be generated.

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 made in the ScanForInstance method:

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;
    }
}

The IdResolver is used for determining what should be used for the id (the value of a single key in a mapping, otherwise the object itself for entities with composite ids).

Like I said, if I remove the call to Get it works fine. It works fine for all other operations as well (create, read, and deletes). All operations, including updating, works fine for entities with single keys.


The DB is Pervasive and there are a certain number of restrictions:

  • No, I cannot change any of the schema (I see that as a frequent response to problems with FNB).
  • I do not want to just delete then insert as there are some columns we do not sync back to our system and I do not want to wipe these out

UPDATED: I've added a simple example that people can copy/paste to test this weird behavior (if it is in fact universal). I'm hoping people will do this to at least confirm my problem.

Type to be mapped, Fluent 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();
    }
}

Don't mind the fact it is called 'ParentType.' I don't actually have any other mappings with this and don't actually use the type as a parent type in this example. It is called this because I'm about to open another question that does involve problems with composite ids and inheritance (DON'T USE COMPOSITE ID'S! :-D).

For the actual testing, I just created a console project in VS with this as the 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 sample is only slightly more helpful since anyone wanting to test this would either need to change the ParentType field to conform to a table that they already have in their own DB, or add a table to match what is mapped in ParentType. I'm hoping someone will do this at least out of curiosity now that I've given a good head-start on testing.

1条回答
爷的心禁止访问
2楼-- · 2019-02-24 09:04

Well, I've at least figured out a solution to my problem, but not why. My solution was to create a new type that encompassed the properties I was using 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, substitute the properties in ParentType for a reference to this new type:

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

    public virtual string SomeField { get; set; }
}

With those changes, the new mapping would 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 made, Merge works even when Get is called prior to the Merge call. My best bet is the non-generic form of CompositeId is not doing something correctly or that the mapping it is making is not working well with NH when you call Merge on an entity that uses it (I'd like to go into the source of FNH to fix it if that is the case but I've already spent too much time figuring out how to bypass this problem).

This is all well and good but this would require me to create a new type for each entity I am mapping, or at least a new type for an id with a different number of keys (ie a type with 2 keys, a type with 3 keys, etc.).

To avoid this, I can hack it so that you add a reference of the same type you are mapping and set the reference 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 would 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 have tested this for updating and inserting using Merge with Get getting called prior to the merge and surprisingly IT WORKS. I'm still on the fence on which fix to use (the new type encompassing the composite id or the self-reference) as the self-reference seems a little to hacky for my tastes.

If anyone finds out WHY this didn't work originally I'd still like to know...

查看更多
登录 后发表回答