Заставить ленивую сущность загрузить реальный экземпляр - PullRequest
6 голосов
/ 30 июля 2009

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

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

Ответы [ 4 ]

21 голосов
/ 30 июля 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).

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

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

Вы также можете создать множество ifs в виде:

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.

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

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

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

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

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

11 голосов
/ 30 июля 2009

Чтобы принудительно получить прокси из базы данных, вы можете использовать метод 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.

3 голосов
/ 14 марта 2011

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

например ..

mapping.Not.LazyLoad ();

или

<class name="OrderLine" table="OrderLine" lazy="false" >
0 голосов
/ 30 июля 2009

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

...