Заставить ленивый объект загрузить реальный экземпляр

У меня есть прокси для ленивого объекта, который был создан в сеансе путем загрузки дочернего объекта. Последующая выборка родительского объекта возвращает только прокси-сервер NH. Мне нужен фактический экземпляр для проверки типа (сущность присоединилась к подклассам). Я должен что-то упустить, но я не могу найти способ сделать это. Session.Refresh(proxy), похоже, не помогает, как и любой вариант HQL, который я пробовал.

Кто-нибудь может помочь?


person Sam    schedule 30.07.2009    source источник
comment
Не могли бы вы показать, как выглядят объекты вашего домена и запросы HQL, которые вы пробовали до сих пор?   -  person Darin Dimitrov    schedule 30.07.2009
comment
Я не уверен, что смогу представить такое количество информации в таком формате. Вы подразумеваете, что должна быть возможность написать запрос HQL, который игнорирует соответствующие прокси-серверы в кеше?   -  person Sam    schedule 03.08.2009


Ответы (4)


Чтобы заставить прокси извлекаться из базы данных, вы можете использовать метод NHibernateUtil.Initialize(proxy) или получить доступ к методу/свойству прокси.

var foo = session.Get<Foo>(id);
NHibernateUtil.Initialize(foo.Bar);

Чтобы проверить, инициализирован объект или нет, вы можете использовать метод NHibernateUtil.IsInitialized(proxy).

Обновление:

Чтобы удалить объект из кэша сеанса, используйте метод Session.Evict(obj).

session.Evict(myEntity);

Информацию о Evict и других методах управления кэшем сеанса можно найти в глава 14.5 документов NHibernate.

person Erik Öjebo    schedule 30.07.2009
comment
Нет, это не работает — похоже, этот метод предназначен для заполнения существующего прокси данными, а не для загрузки нового экземпляра правильного типа. Я предполагаю, что мне нужен механизм для очистки определенного прокси-объекта из кеша (без очистки всего сеанса), чтобы, когда я выполняю Get, он возвращал правильный тип. - person Sam; 03.08.2009
comment
Хорошо, я обновил ответ информацией о том, как удалить объект из кеша сеанса. - person Erik Öjebo; 03.08.2009
comment
Это сработало, Эрик, спасибо - я не знаю, как я пропустил это. - person Sam; 04.08.2009

На мой взгляд, вместо того, чтобы решать эту проблему, вам следует переосмыслить свой дизайн. Вы абсолютно уверены, что в этой ситуации нельзя использовать полиморфизм - либо напрямую назначать сущность ответственной за операцию, которую вы пытаетесь выполнить, либо использовать шаблон посетителя. Я сталкивался с этой проблемой несколько раз и всегда решал изменить дизайн - это приводило к более четкому коду. Я предлагаю вам сделать то же самое, если только вы не уверены, что использование шрифта — лучшее решение.

Проблема

Чтобы получить пример, хотя бы немного похожий на реальный мир, предположим, что у вас есть следующие сущности:

public abstract class Operation
{
    public virtual DateTime PerformedOn { get; set; }
    public virtual double Ammount { get; set; }
}

public class OutgoingTransfer : Operation
{
    public virtual string TargetAccount { get; set; }
}

public class AtmWithdrawal : Operation
{
    public virtual string AtmAddress { get; set; }
}

Естественно, это была бы небольшая часть гораздо большей модели. И теперь вы столкнулись с проблемой: для каждого конкретного типа Операции есть свой способ отображения:

private static void PrintOperation(Operation operation)
{
    Console.WriteLine("{0} - {1}", operation.PerformedOn,
                      operation.Ammount);
}

private static void PrintOperation(OutgoingTransfer operation)
{
    Console.WriteLine("{0}: {1}, target account: {2}",
                      operation.PerformedOn, operation.Ammount,
                      operation.TargetAccount);
}

private static void PrintOperation(AtmWithdrawal operation)
{
    Console.WriteLine("{0}: {1}, atm's address: {2}",
                      operation.PerformedOn, operation.Ammount,
                      operation.AtmAddress);
}

Простые, перегруженные методы будут работать в простом случае:

var transfer = new OutgoingTransfer
               {
                   Ammount = -1000,
                   PerformedOn = DateTime.Now.Date,
                   TargetAccount = "123123123"
               };

var withdrawal = new AtmWithdrawal
                 {
                     Ammount = -1000,
                     PerformedOn = DateTime.Now.Date,
                     AtmAddress = "Some address"
                 };

// works as intended
PrintOperation(transfer);
PrintOperation(withdrawal);

К сожалению, перегруженные методы привязываются во время компиляции, поэтому, как только вы вводите массив/список/любые операции, будет вызываться только универсальная перегрузка (операция).

Operation[] operations = { transfer, withdrawal };
foreach (var operation in operations)
{
    PrintOperation(operation);
}

Есть два решения этой проблемы, и оба имеют недостатки. Вы можете ввести абстрактный/виртуальный метод в Operation для печати информации в выбранный поток. Но это смешает проблемы с пользовательским интерфейсом в вашей модели, поэтому это неприемлемо для вас (сейчас я покажу вам, как вы можете улучшить это решение, чтобы оно соответствовало вашим ожиданиям).

Вы также можете создать множество if в форме:

if(operation is (ConcreteType))
   PrintOperation((ConcreteType)operation);

Это решение уродливо и подвержено ошибкам. Каждый раз, когда вы добавляете/изменяете/удаляете тип операции, вы должны просмотреть каждое место, где вы использовали этот хак, и изменить его. И если вы пропустите одно место, вы, вероятно, сможете поймать только эту среду выполнения - никаких строгих проверок времени компиляции на наличие некоторых ошибок (например, отсутствие одного подтипа).

Кроме того, это решение потерпит неудачу, как только вы введете какой-либо прокси.

Как работает прокси

Приведенный ниже код является ОЧЕНЬ простым прокси (в этой реализации он такой же, как шаблон декоратора, но в целом эти шаблоны не совпадают. Чтобы различить эти два шаблона, потребуется дополнительный код).

public class OperationProxy : Operation
{
    private readonly Operation m_innerOperation;

    public OperationProxy(Operation innerOperation)
    {
        if (innerOperation == null)
            throw new ArgumentNullException("innerOperation");
        m_innerOperation = innerOperation;
    }


    public override double Ammount
    {
        get { return m_innerOperation.Ammount; }
        set { m_innerOperation.Ammount = value; }
    }

    public override DateTime PerformedOn
    {
        get { return m_innerOperation.PerformedOn; }
        set { m_innerOperation.PerformedOn = value; }
    }
}

Как видите, для всей иерархии существует только один прокси-класс. Почему? Потому что вы должны писать свой код так, чтобы он не зависел от конкретного типа — только от предоставленной абстракции. Этот прокси мог бы вовремя отсрочить загрузку сущностей - может быть, вы вообще не будете его использовать? Может быть, вы будете использовать только 2 сущности из 1000? Зачем тогда их всех грузить?

Таким образом, NHibernate использует прокси, как описано выше (хотя и гораздо более сложный), чтобы отложить загрузку объекта. Он может создать 1 прокси для каждого подтипа, но это разрушит всю цель ленивой загрузки. Если вы внимательно посмотрите, как NHibernate хранит подклассы, вы увидите, что для того, чтобы определить тип объекта, вы должны его загрузить. Таким образом, невозможно иметь конкретные прокси — вы можете иметь только самые абстрактные, OperationProxy.

Хотя решение с ifs некрасиво - это было решение. Теперь, когда вы ввели прокси в свою проблему - это больше не работает. Так что остается только полиморфный метод, что неприемлемо из-за смешивания ответственности пользовательского интерфейса с вашей моделью. Давайте исправим это.

Инверсия зависимостей и шаблон посетителя

Во-первых, давайте посмотрим, как будет выглядеть решение с виртуальными методами (только что добавленный код):

public abstract class Operation
{
    public abstract void PrintInformation();
}

public class OutgoingTransfer : Operation
{
    public override void PrintInformation()
    {
        Console.WriteLine("{0}: {1}, target account: {2}",
                      PerformedOn, Ammount, TargetAccount);
    }
}

public class AtmWithdrawal : Operation
{
    public override void PrintInformation()
    {
        Console.WriteLine("{0}: {1}, atm's address: {2}",
                          PerformedOn, Ammount, AtmAddress);
    }
}

public class OperationProxy : Operation
{
    public override void PrintInformation()
    {
        m_innerOperation.PrintInformation();
    }
}

И теперь, когда вы звоните:

Operation[] operations = { transfer, withdrawal, proxy };
foreach (var operation in operations)
{
    operation.PrintInformation();
}

все работает как шарм.

Чтобы удалить эту зависимость пользовательского интерфейса в модели, давайте создадим интерфейс:

public interface IOperationVisitor
{
    void Visit(AtmWithdrawal operation);
    void Visit(OutgoingTransfer operation);
}

Давайте изменим модель, чтобы она зависела от этого интерфейса:

А теперь создайте реализацию — ConsoleOutputOperationVisitor (методы PrintInformation я удалил):

public abstract class Operation
{
    public abstract void Accept(IOperationVisitor visitor);
}

public class OutgoingTransfer : Operation
{
    public override void Accept(IOperationVisitor visitor)
    {
        visitor.Visit(this);
    }
}

public class AtmWithdrawal : Operation
{
    public override void Accept(IOperationVisitor visitor)
    {
        visitor.Visit(this);
    }
}

public class OperationProxy : Operation
{
    public override void Accept(IOperationVisitor visitor)
    {
        m_innerOperation.Accept(visitor);
    }
}

Что здесь происходит? Когда вы вызываете Accept на операции и передаете посетителя, будет вызвана реализация accept, где будет вызвана соответствующая перегрузка метода Visit (компилятор может определить тип "this"). Таким образом, вы комбинируете «мощь» виртуальных методов и перегрузок, чтобы вызвать соответствующий метод. Как видите, теперь ссылка на пользовательский интерфейс здесь, модель зависит только от интерфейса, который может быть включен в слой модели.

Итак, теперь, чтобы заставить это работать, реализация интерфейса:

 public class ConsoleOutputOperationVisitor : IOperationVisitor
 {
    #region IOperationVisitor Members
    public void Visit(AtmWithdrawal operation)
    {
        Console.WriteLine("{0}: {1}, atm's address: {2}",
                          operation.PerformedOn, operation.Ammount,
                          operation.AtmAddress);
    }

    public void Visit(OutgoingTransfer operation)
    {
        Console.WriteLine("{0}: {1}, target account: {2}",
                          operation.PerformedOn, operation.Ammount,
                          operation.TargetAccount);
    }

    #endregion
}

И код:

Operation[] operations = { transfer, withdrawal, proxy };
foreach (var operation in operations)
{
    operation.Accept(visitor);
}

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

person maciejkow    schedule 30.07.2009
comment
В основном я пытаюсь отобразить другую форму (Windows) для каждого подкласса, когда строка дважды щелкается в списке. Я открыт для предложений по улучшению дизайна (который не включает код презентации в модели предметной области). - person Sam; 03.08.2009
comment
Могу я попросить вас подробнее рассказать о полиморфном решении? Моя основная проблема заключается в том, что прокси является производным от базового типа, а не от подкласса, поэтому мне не ясно, как он будет выполнять правильную реализацию операции подкласса. - person Sam; 03.08.2009
comment
Спасибо за объяснение. Лично я не вижу значительных преимуществ в обслуживании этого решения по сравнению с «если obj является подтипом» с явной проверкой прокси - это компромисс между воспринимаемым уродством и большим количеством кода. Обратите внимание, что мои подклассы в этой ситуации очень ориентированы на данные - у меня может быть другое мнение в другом сценарии. - person Sam; 04.08.2009

Отключение отложенной загрузки приведет к возврату фактического экземпляра вместо прокси-сервера NHibernate.

eg..

сопоставление.Not.LazyLoad();

or

<class name="OrderLine" table="OrderLine" lazy="false" >
person sjclark76    schedule 13.03.2011

Поскольку прокси является производным от класса сущности, вы, вероятно, можете просто проверить entity.GetType().BaseType, чтобы получить определенный тип.

person Paul Alexander    schedule 30.07.2009
comment
Нет, он не может. объект присоединился к подклассам. Это означает, что прокси всегда будет производным от БАЗОВОГО класса этой иерархии, а не от конкретного класса (в то время, когда nhibernate создает прокси, он не знает, какой тип объекта!). Сэм хочет проверить, какой из подтипов это. - person maciejkow; 30.07.2009
comment
Это имеет смысл ... пропустил это. - person Paul Alexander; 31.07.2009