NHibernate QueryOver на IUserType - PullRequest
       20

NHibernate QueryOver на IUserType

4 голосов
/ 10 января 2012

Прежде всего позвольте мне извиниться за длину этого поста, хотя в основном это код, поэтому я надеюсь, что вы все терпите меня!

У меня есть сценарий работы с устаревшей базой данных, где мне нужно былонаписать IUserType, используя NHibernate 3.2, чтобы взять 2-символьное поле «status» и вернуть из него логическое значение.Поле состояния может содержать 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
)

Свободное отображение:

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.

...
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?

Заранее всем спасибо!

1 Ответ

2 голосов
/ 11 января 2012

Решено!

После того, как я отправил свой вопрос, я вернулся к чертежной доске и нашел решение, которое не требует взлома сгенерированного SQL в реализации IUserType.На самом деле этому решению вообще не нужен IUserType!

Вот что я сделал.

Сначала я изменил столбец IsActive, чтобы использовать формулу для обработки проверки нуля.Это исправило мою проблему с ошибкой QueryOver, потому что теперь каждый раз, когда NHibernate обрабатывает свойство IsActive, он вводит мою формулу sql для обработки нулевого значения.

Недостатком этого подхода было то, что после того, как я вставил в формулу все мои тесты сохраненияне удалось.Оказывается, что свойства формул фактически являются свойствами ReadOnly.

Итак, чтобы обойти эту проблему, я добавил защищенное свойство к объекту для хранения значения состояния из базы данных.

Затем я изменил свойство IsActive, чтобы установить для свойства защищенного состояния значение "" или "DI".И, наконец, я изменил FluentMapping, чтобы Показывать защищенное свойство Status на NHibernate, чтобы NHibernate мог отслеживать его.Теперь, когда NHibernate знает о статусе, он может включить его в свои операторы INSERT / UPDATE.

Я собираюсь включить мое решение ниже на случай, если кому-то еще будет интересно.

Класс клиента

public class Client 
{
    ...

    protected virtual string Status { get; set; }
    private bool _isActive;
    public virtual bool IsActive
    {
        get { return _isActive; }
        set
        {
            _isActive = value;
            Status = (_isActive) ? "  " : "DI";
        }
    }
}

Изменения в отображении флюентов

public class ClientMapping : CoreEntityMapping<Client>
{
    public ClientMapping()
    {
        ....

        Map(Reveal.Member<E>("Status"), colName).Length(2);
        Map(x => x.IsActive).Formula("case when clnStatus is null then '  ' else clnStatus end");
    }
}
...