2012-01-09 4 views
4

Прежде всего позвольте мне извиниться за длину этого сообщения, это в основном код, хотя я надеюсь, что вы все несете со мной!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?

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

ответ

2

Решено!

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

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

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

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

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

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

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

класс Client

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"; 
     } 
    } 
} 

Изменения Fluent Mapping

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"); 
    } 
} 
Смежные вопросы