4

Is it possible to map a database column to a constant value without the need for a property in the entity class? This basically is a workaround for a missing default value on that column in the database in combination with a NOT NULL constrained. The database is external and can't be changed but I don't need all of the columns in that table and thus don't want to have corresponding properties in my entity class.

I am asking basically the same as described in this Hibernate JIRA issue.

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443

3 Answers3

2

Based on Firos answer I solved the problem. However, I didn't quite like the syntax to be used and the fact that I would have to create a new class for the default values for each entity.

The syntax I got now looks like this:

mapping.ConstantValue(0).Column(@"client_id");
// or
mapping.ConstantValue(0, @"client_id");

I created the following extension methods for it:

public static PropertyPart
   ConstantValue<TType, TValue>(this ClasslikeMapBase<TType> map, TValue value)
{
    var getter =
        new ConstantValueGetter<TValue>(CreateUniqueMemberName(), value);
    ConstantValueAccessor.RegisterGetter(typeof(TType), getter);

    var propertyInfo =
        new GetterSetterPropertyInfo(typeof(TType), typeof(TValue), 
                                     getter.PropertyName, getter.Method, null);

    var parameter = Expression.Parameter(typeof(TType), "x");
    Expression body = Expression.Property(parameter, propertyInfo);
    body = Expression.Convert(body, , typeof(object));

    var lambda = Expression.Lambda<Func<TType, object>>(body, parameter);

    return map.Map(lambda).Access.Using<ConstantValueAccessor>();
}

public static PropertyPart
   ConstantValue<TType, TValue>(this ClasslikeMapBase<TType> map,
                                TValue value, string column)
{
    return map.ConstantValue(value).Column(column);
}

The important differences are:

  1. The first of those extension methods returns a PropertyPart and has to be used in conjunction with the Column method to specify which column the constant value should be mapped to. Because of this, the column name is not known when the extension method is executed and we need to create one ourselves. This is done by CreateUniqueMemberName:

    private static string CreateUniqueMemberName()
    {
        return "Dummy" + Guid.NewGuid().ToString("N");
    }
    
  2. Because you can only specify a type as access strategy and not an instance, I couldn't create an IPropertyAccessor implementation allowed me to simply pass an IGetter instance in the constructor. That's what ConstantValueAccessor.RegisterGetter(typeof(TType), getter); solves. ConstantValueAccessor has a static collection of getters:

    internal class ConstantValueAccessor : IPropertyAccessor
    {
        private static readonly
        ConcurrentDictionary<Type, SynchronizedCollection<IGetter>> _getters =
            new ConcurrentDictionary<Type, SynchronizedCollection<IGetter>>();
    
        public static void RegisterGetter(Type type, IGetter getter)
        {
            var getters =
                _getters.GetOrAdd(type,
                                  t => new SynchronizedCollection<IGetter>());
            getters.Add(getter);
        }
    
        public IGetter GetGetter(Type theClass, string propertyName)
        {
            SynchronizedCollection<IGetter> getters;
            if (!_getters.TryGetValue(theClass, out getters))
                return null;
            return getters.SingleOrDefault(x => x.PropertyName == propertyName);
        }
    
        // ...
    }
    

The implementation of ConstantValueGetter<T> is the same as the one from the provided link.

Because it wasn't that much fun to implement GetterSetterPropertyInfo, here it is. One important difference is, that this implementation doesn't have any dependencies on (Fluent) NHibernate.

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
  • Very nice indeed. I would favor your approach however i can't use .NET 4 and i wanted to support the case when the configuration object is (de)serialized (100+ entities), which is not supported with your registration in the mappings. same as when xmlmappings are created from fluent and read in as xml. – Firo Jan 28 '12 at 22:27
  • @Firo: I see. It is not supported because of the static `_getters` `Dictionary`, right? Or am I missing something? – Daniel Hilgarth Jan 29 '12 at 07:59
  • yes because the registration is done in the mapping classes which are not used when deserializing the configuration object. ConcurrentDictionary is .net 4 only – Firo Jan 29 '12 at 15:52
1

If you don't want to introduce property in your entity class the only solution I see is to create custom property accessor which will always return constant value. Here is possible implementation:

public class ConstantAccessor : IPropertyAccessor
{
    #region IPropertyAccessor Members

    public IGetter GetGetter(Type theClass, string propertyName)
    {
        return new ConstantGetter();
    }

    public ISetter GetSetter(Type theClass, string propertyName)
    {
        return new NoopSetter();
    }

    public bool CanAccessThroughReflectionOptimizer
    {
        get { return false; }
    }

    #endregion

    [Serializable]
    private class ConstantGetter : IGetter
    {
        #region IGetter Members

        public object Get(object target)
        {
            return 0; // Always return constant value
        }

        public Type ReturnType
        {
            get { return typeof(object); }
        }

        public string PropertyName
        {
            get { return null; }
        }

        public MethodInfo Method
        {
            get { return null; }
        }

        public object GetForInsert(object owner, IDictionary mergeMap,
                                               ISessionImplementor session)
        {
            return null;
        }

        #endregion
    }

    [Serializable]
    private class NoopSetter : ISetter
    {
        #region ISetter Members

        public void Set(object target, object value)
        {
        }

        public string PropertyName
        {
            get { return null; }
        }

        public MethodInfo Method
        {
            get { return null; }
        }

        #endregion
    }
}

Here how to use it:

<property name="Value"
          access="ConsoleApplication2.ConstantAccessor, ConsoleApplication2"
          column="a_value" type="int" />

Property "Value" doesn't need to exist in your entity. It is here because attribute "name" is required.

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
hival
  • 675
  • 4
  • 5
  • That looks promising. I will test it in a few hours. Can you provide a fluent mapping instead of an HBM mapping? Thanks. – Daniel Hilgarth Jan 25 '12 at 10:17
  • OK. Thanks anyway. If I am able to translate it to fluent mapping, I will post it. – Daniel Hilgarth Jan 25 '12 at 10:29
  • It looks like it's not possible with Fluent NHibernate, because the starting point for any mapping is a property... – Daniel Hilgarth Jan 25 '12 at 21:36
  • 1
    @DanielHilgarth it is possible with FNH. I implemented it in a "going to production" app ;) if you are still interested i think i could post some code – Firo Jan 27 '12 at 13:58
  • @Firo: Indeed, I am very interested! – Daniel Hilgarth Jan 27 '12 at 14:01
  • I think it can be used with mapping by code like this: Property("Value", m => { m.Column("a_value"); m.Access(typeof(ConstantAccessor)); }); – hival Jan 27 '12 at 15:49
  • @hival where is `Property(string` defined? – Firo Jan 27 '12 at 16:58
  • Maybe we are talking about defferent things. NH3.2 -> namespace NHibernate.Mapping.ByCode.Impl.CustomizersImpl -> class PropertyContainerCustomizer -> public void Property(string notVisiblePropertyOrFieldName, Action mapping); – hival Jan 27 '12 at 18:55
  • @hival that won't work because in implementation there is `MemberInfo member = GetPropertyOrFieldMatchingNameOrThrow(notVisiblePropertyOrFieldName);` – Firo Jan 28 '12 at 22:13
1

My implementation takes the same idea as hival but goes a lot further. the basis is an implementation of IPropertyAccessor

/// <summary>
/// Defaultvalues für nicht (mehr) benötigte Spalten siehe
/// http://elegantcode.com/2009/07/13/using-nhibernate-for-legacy-databases/
/// </summary>
public abstract class DefaultValuesBase : IPropertyAccessor
{
    public abstract IEnumerable<IGetter> DefaultValueGetters { get; }

    public bool CanAccessThroughReflectionOptimizer
    {
        get { return false; }
    }

    public IGetter GetGetter(Type theClass, string propertyName)
    {
        return DefaultValueGetters.SingleOrDefault(getter => getter.PropertyName == propertyName);
    }

    public ISetter GetSetter(Type theClass, string propertyName)
    {
        return new NoopSetter();
    }
}

// taken from the link
[Serializable]
public class DefaultValueGetter<T> : IGetter {...}

// ---- and the most tricky part ----
public static void DefaultValues<T>(this ClasslikeMapBase<T> map, DefaultValuesBase defaults)
{
    DefaultValuesInternal<T>(map.Map, defaults);
}

public static void DefaultValues<T>(this CompositeElementPart<T> map, DefaultValuesBase defaults)
{
    DefaultValuesInternal<T>(map.Map, defaults);
}

private static void DefaultValuesInternal<T>(
    Func<Expression<Func<T, object>>, PropertyPart> mapFunction, DefaultValuesBase defaults)
{
    var noopSetter = new NoopSetter();
    var defaultsType = defaults.GetType();

    foreach (var defaultgetter in defaults.DefaultValueGetters)
    {
        var parameter = Expression.Parameter(typeof(T), "x");
        Expression body = Expression.Property(parameter,
            new GetterSetterPropertyInfo(typeof(T), defaultgetter, noopSetter));

        body = Expression.Convert(body, typeof(object));

        var lambda = Expression.Lambda<Func<T, object>>(body, parameter);

        mapFunction(lambda).Column(defaultgetter.PropertyName).Access.Using(defaultsType);
    }
}

// GetterSetterPropertyInfo inherits PropertyInfo with important part
public override string Name
{
    get { return m_getter.PropertyName; } // propertyName is the column in db
}

// and finally in SomeEntityMap
this.DefaultValues(new SomeEntityDefaults());

public class SomeEntityDefaults : DefaultValuesBase
{
    public override IEnumerable<IGetter> DefaultValueGetters
    {
        get
        {
            return new [] {
                new DefaultValueGetter<int>("someColumn", 1),
                new DefaultValueGetter<string>("somestrColumn", "empty"),
            };
        }
    }
}
Firo
  • 30,626
  • 4
  • 55
  • 94