1
votes

I have an NHibernate implementation working with a legacy database (DB2) that I have been asked to modify and I'm running into an issue with getting an id generator to work on an id property that is defined as a customer user type.

The data in a table is mapped to a class similar to the following:

// CLASS FILE
public class Request {
    public virtual int Id { get; set; }
    ... other data properties ...
}

//MAPPING FILE
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="LibraryName" namespace="LibraryName.Domain" default-cascade="none">
    <class name="Request" schema="..." table="..." where="...">
        <id name="Id" column="..." type="LibraryName.Domain.DB2UserTypes.NullableIntegerType, LibraryName" />

        ... other data property mappings ...


The legacy database has tables with columns that are foreign keys into the Request's table. The columns containing these foreign keys are NOT nullable so the custom NullableIntegerType is used to write a zero into the foreign key column when a Request object property is null in a referencing object.

// NullableIntegerType Definition
public class NullableIntegerType : IEnhancedUserType
{
    private static readonly SqlType[] SQL_TYPES = { NHibernate.NHibernateUtil.Int32.SqlType };
    public SqlType[] SqlTypes { get { return SQL_TYPES; } }

    public new bool Equals(object x, object y)
    {
        if (object.ReferenceEquals(x, y))
            return true;
        else if ((x == null) && (y == null))
            return true;
        else if ((x == null) || (y == null))
            return false;
        else
            return x.Equals(y);
    }

    public object DeepCopy(object value) { return value; }
    public bool IsMutable { get { return false; } }
    public object Assemble(object cached, object owner) { return cached; }
    public object Disassemble(object value) { return value; }
    public object Replace(object original, object target, object owner) { return original; }
    public int GetHashCode(object obj) { return obj.GetHashCode(); }

    public Type ReturnedType { get { return typeof(int); } }

    public object NullSafeGet(IDataReader dr, string[] names, object owner)
    {
        object obj = NHibernate.NHibernateUtil.Int32.NullSafeGet(dr, names[0]);
        if (obj == null)
            return null;
        else
        {
            int result = (int)obj;

            if ((result == 0) || (result == 9999999))
                return null;
            else
            {
                return int.Parse(result.ToString());
            }
        }
    }

    public void NullSafeSet(IDbCommand cmd, object obj, int index)
    {
        if (obj == null)
            NHibernateUtil.Int32.NullSafeSet(cmd, 0, index);
        else
            NHibernateUtil.Int32.NullSafeSet(cmd, obj, index);
    }

    public object FromXMLString(string xml)
    {
        return int.Parse(xml);
    }

    public string ToXMLString(object obj)
    {
        return ((int)obj).ToString();
    }

    public string ObjectToSQLString(object obj)
    {
        return ((int)obj).ToString();
    }
}


I have been asked to modify the mapping so that we can create new Request objects and have the ID be generated using a generator. I created the generator class and modified the Request object mapping to use the generator for the Id.

// GENERATOR CLASS
public class RequestGenerator : TableGenerator
{
    public override object Generate(NHibernate.Engine.ISessionImplementor session, object obj)
    {
        using (IDbCommand command = session.Connection.CreateCommand())
        {
            command.CommandText = @"...";
            command.CommandType = CommandType.StoredProcedure;

            var parameter = command.CreateParameter();
            parameter.DbType = DbType.Int32;
            parameter.ParameterName = "@generatedId";
            parameter.Value = 0;
            parameter.Direction = ParameterDirection.InputOutput;
            command.Parameters.Add(parameter);

            command.ExecuteNonQuery();

            return Convert.ToInt32(parameter.Value);
        }
    }
}

//MODIFIED MAPPING FILE
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="LibraryName" namespace="LibraryName.Domain" default-cascade="none">
    <class name="Request" schema="..." table="..." where="...">
        <id name="Id" column="..." type="LibraryName.Domain.DB2UserTypes.NullableIntegerType, LibraryName" >
            <generator class="LibraryName.Domain.DB2Generators.RequestGenerator, LibraryName" />
        </id>

        ... other data property mappings ...


When I attempt to test the modified mapping I get an error before my code even runs that NHibernate is unable to instantiate the ID generator.

// Sample Code
Request request = new Request() { Id = 0, ... other properties }
session.Save(request);
session.Flush();

// Exception Details and Stack Trace
NHibernate.MappingException was unhandled by user code
Message=could not instantiate id generator: LibraryName.Domain.DB2Generators.RequestGenerator, LibraryName
Source=NHibernate
StackTrace:
    at NHibernate.Id.IdentifierGeneratorFactory.Create(String strategy, IType type, IDictionary`2 parms, Dialect dialect)
    at NHibernate.Mapping.SimpleValue.CreateIdentifierGenerator(Dialect dialect, String defaultCatalog, String defaultSchema, RootClass rootClass)
    at NHibernate.Impl.SessionFactoryImpl..ctor(Configuration cfg, IMapping mapping, Settings settings, EventListeners listeners)
    at NHibernate.Cfg.Configuration.BuildSessionFactory()
    at LibraryName.Infrastructure.NH.NHibernateSessionFactory.createSessionFactory() in C:\...\NHibernate\NHibernateSessionFactory.cs:line 23
    at LibraryName.Infrastructure.NH.NHibernateSessionFactory.get_SessionFactory() in C:\...\NHibernate\NHibernateSessionFactory.cs:line 15
    at LibraryName.Infrastructure.NH.NHibernateSessionFactoryProvider.CreateInstance(IContext context) in C:\...\NHibernate\NHibernateSessionFactoryProvider.cs:line 13
    at Ninject.Activation.Provider`1.Create(IContext context)
    at Ninject.Activation.Context.Resolve()
    at Ninject.KernelBase.<>c__DisplayClass10.<Resolve>b__c(IBinding binding)
    at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
    at System.Linq.Enumerable.<CastIterator>d__b1`1.MoveNext()
    at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
    at Ninject.ResolutionExtensions.Get[T](IResolutionRoot root, IParameter[] parameters)
    at LibraryName.Infrastructure.InternalMvcModule.<Load>b__0(IContext c) in C:\...\Ninject\InternalMvcModule.cs:line 16
    at Ninject.Activation.Providers.CallbackProvider`1.CreateInstance(IContext context)
    at Ninject.Activation.Provider`1.Create(IContext context)
    at Ninject.Activation.Context.Resolve()
    at Ninject.KernelBase.<>c__DisplayClass10.<Resolve>b__c(IBinding binding)
    at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
    at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source)
    at Ninject.Planning.Targets.Target`1.GetValue(Type service, IContext parent)
    at Ninject.Planning.Targets.Target`1.ResolveWithin(IContext parent)
    at Ninject.Activation.Providers.StandardProvider.GetValue(IContext context, ITarget target)
    at Ninject.Activation.Providers.StandardProvider.<>c__DisplayClass4.<Create>b__2(ITarget target)
    at System.Linq.Enumerable.WhereSelectArrayIterator`2.MoveNext()
    at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
    at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
    at Ninject.Activation.Providers.StandardProvider.Create(IContext context)
    at Ninject.Activation.Context.Resolve()
    at Ninject.KernelBase.<>c__DisplayClass10.<Resolve>b__c(IBinding binding)
    at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
    at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source)
    at Ninject.Web.Mvc.NinjectDependencyResolver.GetService(Type serviceType)
    at System.Web.Mvc.DefaultControllerFactory.DefaultControllerActivator.Create(RequestContext requestContext, Type controllerType)


    InnerException: System.ArgumentException
    Message=type is not a ValueTypeType
    Parameter name: type
    Source=NHibernate
    ParamName=type
    StackTrace:
        at NHibernate.Id.TableGenerator.Configure(IType type, IDictionary`2 parms, Dialect dialect)
        at NHibernate.Id.IdentifierGeneratorFactory.Create(String strategy, IType type, IDictionary`2 parms, Dialect dialect)
        InnerException: 

If I remove the custom type (type="LibraryName.Domain.DB2UserTypes.NullableIntegerType, LibraryName") from the id mapping then the generator works correctly and my object saves to the database. How can I make the generator and the user defined type on the Id work together?

1

1 Answers

1
votes

The NHibernate sources of the TableGenerator class show that its Configure method expects the column type to be a subclass of PrimitiveType. In your case this seems not to be the case, because you simply implement IEnhancedUserType (which probably get's wrapped internally, but by something which is not a PrimitiveType).

I cannot explain the reason for this, but in one of my projects I face similar requirements, i.e. implementing 0/null magic in a custom NHibernate type and using a custom id generator. In my project the custom "id" type directly implements IUserType and the id generator directly implements IIdentifierGenerator and IConfigurable, i.e. it does not extend NHibernate's TableGenerator. Maybe you should do the same with your id generator in order to work around the column type restriction of NHibernate's TableGenerator.