Прежде всего позвольте мне извиниться за длину этого сообщения, это в основном код, хотя я надеюсь, что вы все несете со мной!NHibernate QueryOver на IUserType
У меня есть сценарий работы с устаревшей базой данных, где мне нужно было написать IUserType с помощью NHibernate 3.2, чтобы взять 2-значное поле статуса и вернуть из него логическое значение. Поле состояния может содержать 3 возможных значения:
* 'DI' // 'Disabled', return false
* ' ' // blank or NULL, return true
* NULL
Вот что я упростил.
Таблица Определение:
CREATE TABLE [dbo].[Client](
[clnID] [int] IDENTITY(1,1) NOT NULL,
[clnStatus] [char](2) NULL,
[clnComment] [varchar](250) NULL,
[clnDescription] [varchar](150) NULL,
[Version] [int] NOT NULL
)
Fluent Mapping:
public class ClientMapping : CoreEntityMapping<Client>
{
public ClientMapping()
{
SchemaAction.All().Table("Client");
LazyLoad();
Id(x => x.Id, "clnId").GeneratedBy.Identity();
Version(x => x.Version).Column("Version").Generated.Never().UnsavedValue("0").Not.Nullable();
OptimisticLock.Version();
Map(x => x.Comment, "clnComment").Length(250).Nullable();
Map(x => x.Description, "clnDescription").Length(250).Nullable();
Map(x => x.IsActive, "clnStatus").Nullable().CustomType<StatusToBoolType>();
}
}
Мой IUserType Реализация:
public class StatusToBoolType : IUserType
{
public bool IsMutable { get { return false; } }
public Type ReturnedType { get { return typeof(bool); } }
public SqlType[] SqlTypes { get { return new[] { NHibernateUtil.String.SqlType }; } }
public object DeepCopy(object value)
{
return value;
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return cached;
}
public object Disassemble(object value)
{
return value;
}
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;
return x.Equals(y);
}
public int GetHashCode(object x)
{
return x == null ? typeof(bool).GetHashCode() + 473 : x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);
if (obj == null) return true;
var status = (string)obj;
if (status == " ") return true;
if (status == "DI") return false;
throw new Exception(string.Format("Expected data to be either empty or 'DI' but was '{0}'.", status));
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var parameter = ((IDataParameter) cmd.Parameters[index]);
var active = value == null || (bool) value;
if (active)
parameter.Value = " ";
else
parameter.Value = "DI";
}
}
Однако это не сработает. Этот модульный тест выходит из строя с неточным подсчетом.
[TestMethod]
public void GetAllActiveClientsTest()
{
//ACT
var count = Session.QueryOver<Client>()
.Where(x => x.IsActive)
.SelectList(l => l.SelectCount(x => x.Id))
.FutureValue<int>().Value;
//ASSERT
Assert.AreNotEqual(0, count);
Assert.AreEqual(1721, count);
}
Причина она не потому, что он генерирует следующий SQL:
SELECT count(this_.clnID) as y0_ FROM Client this_ WHERE this_.clnstatus = @p0;
/* @p0 = ' ' [Type: String (0)] */
Но мне нужно, чтобы генерировать вместо этого:
SELECT count(this_.clnID) as y0_ FROM Client this_ WHERE (this_.clnstatus = @p0 <b> OR this_.clnstatus IS NULL);</b>
После некоторых отладки я увидел, что NullSafeSet() в моем классе StatusToBoolType вызывается перед созданием запроса, поэтому мне удалось обойти это, написав некоторый хакерский код в этом методе, чтобы манипулировать SQL в cmd.CommandText p roperty.
...
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var parameter = ((IDataParameter) cmd.Parameters[index]);
var active = value == null || (bool) value;
if (active)
{
parameter.Value = " ";
if (cmd.CommandText.ToUpper().StartsWith("SELECT") == false) return;
var paramindex = cmd.CommandText.IndexOf(parameter.ParameterName);
if (paramindex > 0)
{
// Purpose: change [columnName] = @p0 ==> ([columnName] = @p0 OR [columnName] IS NULL)
paramindex += parameter.ParameterName.Length;
var before = cmd.CommandText.Substring(0, paramindex);
var after = cmd.CommandText.Substring(paramindex);
//look at the text before the '= @p0' and find the column name...
var columnSection = before.Split(new[] {"= " + parameter.ParameterName}, StringSplitOptions.RemoveEmptyEntries).Reverse().First();
var column = columnSection.Substring(columnSection.Trim().LastIndexOf(' ')).Replace("(", "");
var myCommand = string.Format("({0} = {1} OR {0} IS NULL)", column.Trim(), parameter.ParameterName);
paramindex -= (parameter.ParameterName.Length + column.Length + 1);
var orig = before.Substring(0, paramindex);
cmd.CommandText = orig + myCommand + after;
}
}
else
parameter.Value = "DI";
}
Но это NHibernate !!! Взлом инструкции sql вроде этого не может быть правильным способом справиться с этим? Правильно?
Поскольку это общая устаревшая база данных, я не могу изменить схему таблицы NOT NULL, иначе я бы просто сделал это и избегал этого сценария.
Итак, наконец, после всей этой прелюдии мой вопрос заключается именно в этом, где я могу сообщить NHibernate о создании пользовательского выражения критериев SQL для этого IUserType?
Спасибо вам заранее!