NHibernate: a custom (parametric) UserType to truncate strings

Print Content | More

Developing Dexter I encountered again a usual usual error you have to deal with NHibernate and strings: we tried to persist en entity whose string field exceeded the limit imposed by the database table; Nhibernate rightfully complained with:

System.Data.SqlClient.SqlException: String or binary data would be truncated. The statement has been terminated.

Given the fact that having partial data for these fields was acceptable we decided to truncate the value itself. Instead of using the usual solution - that is: take care of this in the entity class or somewhere else during the validation - I decided to build some custom UserType and let NHibernate take care of the truncation if needed (this way we do not have to change anything in the logic of the application).

Why this approach ? well digging into NHibernate code you can see that it uses a lot of structures very similar to a UserType to actually take care of the interaction between the mapped properties and the database (look at the NHibernate.Type namespace in the source code or with Reflector), adding another one isn’t a big issue so I decided to follow their approach and inherited directly from AbstractStringType:

public abstract class AbstractTruncatedStringType : AbstractStringType
{
	internal AbstractTruncatedStringType()
		: base(new StringSqlType())
	{
	}

	internal AbstractTruncatedStringType(StringSqlType sqlType)
		: base(sqlType)
	{
	}

	public abstract int Length { get; }

	public override void Set(System.Data.IDbCommand cmd, object value, int index)
	{
		string str = (string)value;
		if (str.Length > Length)
			str = str.Substring(0, Length);
		base.Set(cmd, str, index);
	}
}

and then added some specializations:

public class TruncatedString500 : AbstractTruncatedStringType
{
	public override int Length
	{
		get { return 500; }
	}

	public override string Name
	{
		get { return "TruncatedString500"; }
	}
}

public class TruncatedString100 : AbstractTruncatedStringType
{
	public override int Length
	{
		get { return 100; }
	}

	public override string Name
	{
		get { return "TruncatedString100"; }
	}
}

...

you can use these classes writing your mapping like this:

...
<class name="Data" table="DATA">
		<id name="Id" column="ID" type="guid" unsaved-value="00000000-0000-0000-0000-000000000000">
			<generator class="guid" />
		</id>
		<property name="Data1" column="DATA1" type="string" length="100" />
		<property name="TruncatedString" column="TruncatedString" length="10" type="Structura.NHibernate.UserTypes.TruncatedString10, Structura.NHibernate" />
		...

Which is good...but not enough for me...you see we need to implement a lot of different versions of this class based on the limit we want to impose to the string, why not make this type parametric then !? We can do it just implementing the IParameterizedType interface.

The code is quite straightforward to write:

public class TruncatedString : AbstractTruncatedStringType, IParameterizedType
{
	private const int DefaultLimit = 50;

	public override int Length
	{
		get { return _length; }
	}
	private int _length = DefaultLimit;

	public override string Name
	{
		get { return "TruncatedString"; }
	}

	public void SetParameterValues(System.Collections.Generic.IDictionary<string, string> parameters)
	{
		if (false == int.TryParse(parameters["length"], out _length))
			_length = DefaultLimit;
	}
}

And this is how you can use it in your mappings:

...
<property name="TruncatedString" column="TruncatedString" length="10">
	<type name="Structura.NHibernate.UserTypes.TruncatedString, Structura.NHibernate">
		<param name="length">10</param>
	</type>
</property>
...

Yet not perfect...but it’s an improvement, so you have two choices: implement multiple versions of the abstract class (and keep your mapping cleaner) or use the parameterized version (and have extra flexibility).



Nhibernate, UserType, Truncate, String

3 comments

Related Post

  1. #1 da Andrea Balducci - Friday March 2010 alle 10:30

    you can "inject" the length at run time reading the mappings.

    PersistentClass pc = _configuration.GetClassMapping(objType);
    Property mappingProperty = pc.GetProperty(property);
    int nLen = 0;

    if(mappingProperty.IsBasicPropertyAccessor)
    {
    foreach (Column col in mappingProperty.ColumnCollection)
    {
    if (col.Type.ReturnedClass == typeof(string))
    {
    nLen = col.Length;
    break;
    }
    }
    }

  2. #2 da Alessandro Giorgetti - Friday March 2010 alle 10:49

    Cool, I missed that, I'll add that too :D

  3. #3 da Alessandro Giorgetti - Friday March 2010 alle 02:58

    I've spent the last hour doing research on this topic, cause we already have a SqlType member in the AbstractStringType class and in its base classes (so there's no need to scan the configuration mapping, and to be honest I have no idea on how to access the configuration object inside a UserType)...if it only would have been populated in the correct way!

    I digged inside NHibernate code to find why he actually wasn't using the second constructor of the class (the one that takes the fully populated StringSqlObject..as I expected at first), in short:
    the column of the mapping is handled by the 'SimpleValue' class which have a this method to create the NHibernate.Type:

    private IType GetHeuristicType()
    {
    // NH different behavior
    // If the mapping has a type as "Double(10,5)" our SqlType will be created with all information
    // including the rigth SqlType specification but when the length/presion/scale was specified
    // trough attributes the SqlType is wrong (does not include length/presion/scale specification)

    IType result = null;
    if (ColumnSpan == 1 && !columns[0].IsFormula)
    {
    var col = (Column) columns[0];
    if(col.IsLengthDefined())
    {
    result = TypeFactory.BuiltInType(typeName, col.Length);
    }
    else if(col.IsPrecisionDefined())
    {
    result = TypeFactory.BuiltInType(typeName, Convert.ToByte(col.Precision), Convert.ToByte(col.Scale));
    }
    }
    return result ?? TypeFactory.HeuristicType(typeName, typeParameters);
    }

    as you can see the length parameter is passed only to the built-ins type (which construct the types using a 'well-formed' SqlType), and for any other custom type (handled by the TypeFactory.HeuristicType() function) we only can play with the type parameters.

    Looking at my code after reviewing this, it's clear to me that the only constructor really needed here is the parameter-less one.

    I'm open for any suggestion or fix that can improve this custom type even more.

All fields are required and you must provide valid data in order to be able to comment on this post.


(will not be published)
(es: http://www.mysite.com)


  1. #1 da http://topsy.com/trackback?url=http://www.primordialcode.com/blog/post/nhibernate-custom-parametric-usertype-truncate-strings

    Twitter Trackbacks for www.primordialcode.com - NHibernate: a custom (parametric) UserType to truncate strings [primordialcode.com] on Topsy.com