8

我有一个惰性实体的代理,它是通过加载子实体在会话中创建的。对父实体的后续提取仅返回 NH 代理。我需要实际的实例来检查类型(实体已加入子类)。我一定错过了一些东西,但我找不到办法做到这一点。Session.Refresh(proxy) 似乎没有帮助,我尝试过的任何 HQL 风格也没有帮助。

任何人都可以帮忙吗?

4

4 回答 4

21

在我看来,与其解决这个问题,不如重新考虑你的设计。您是否绝对确定,在这种情况下您不能使用多态性 - 直接让实体负责您尝试执行的操作或使用访问者模式。我几次遇到这个问题并且总是决定改变设计 - 它导致更清晰的代码。我建议你也这样做,除非你绝对确定依赖类型是最好的解决方案。

问题

为了让示例至少与现实世界有些相似,假设您有以下实体:

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

这个问题有两种解决方案,而且都有缺点。您可以在操作中引入抽象/虚拟方法以将信息打印到选定的流。但这会将 UI 问题混入您的模型中,因此您无法接受(稍后我将向您展示如何改进此解决方案以满足您的期望)。

您还可以以以下形式创建许多 if:

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

这个解决方案丑陋且容易出错。每次添加/更改/删除操作类型时,您都必须遍历使用这些 hack 的每个地方并对其进行修改。如果你错过了一个地方,你可能只能捕捉到那个运行时——没有对某些错误进行严格的编译时检查(比如缺少一个子类型)。

此外,一旦您引入任何类型的代理,此解决方案就会失败。

代理的工作原理

下面的代码是非常简单的代理(在这个实现中它与装饰器模式相同 - 但这些模式通常不一样。需要一些额外的代码来区分这两种模式)。

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

如您所见 - 整个层次结构只有一个代理类。为什么?因为您应该以不依赖于具体类型的方式编写代码 - 仅依赖于提供的抽象。这个代理可以及时延迟实体加载——也许你根本不会使用它?也许您只会使用 1000 个实体中的 2 个?为什么要加载它们呢?

所以 NHibernate 使用上面的代理(虽然更复杂)来延迟实体加载。它可以为每个子类型创建 1 个代理,但它会破坏延迟加载的整个目的。如果您仔细查看 NHibernate 如何存储您会看到的子类,为了确定实体类型是什么,您必须加载它。所以不可能有具体的代理——你只能有最抽象的,OperationProxy。

尽管使用 ifs 的解决方案很丑 - 这是一个解决方案。现在,当您为您的问题引入代理时 - 它不再起作用。所以这只是给我们留下了多态方法,这是不可接受的,因为你的模型混合了 UI 责任。让我们解决这个问题。

依赖倒置和访问者模式

首先,让我们看看使用虚拟方法的解决方案是什么样的(只是添加了代码):

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

所有的作品都是一种魅力。

为了移除模型中的这个 UI 依赖,让我们创建一个接口:

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”的类型)。因此,您结合了虚拟方法和重载的“力量”来调用适当的方法。如您所见 - 现在这里是 UI 参考,模型只依赖于一个接口,它可以包含在模型层中。

所以现在,为了让它工作,接口的实现:

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

我很清楚这不是一个完美的解决方案。在添加新类型时,您仍然需要修改界面和访问者。但是你会得到编译时检查,并且永远不会错过任何东西。使用这种方法很难实现的一件事是获得可插入的子类型——但我不相信这是一个有效的场景。您还必须修改此模式以满足您在具体场景中的需求,但我将把它留给您。

于 2009-07-30T07:37:43.310 回答
12

要强制从数据库中获取代理,您可以使用该NHibernateUtil.Initialize(proxy)方法,或访问代理的方法/属性。

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

要检查对象是否已初始化,可以使用该NHibernateUtil.IsInitialized(proxy)方法。

更新:

要从会话缓存中删除对象,请使用Session.Evict(obj)方法。

session.Evict(myEntity);

有关管理会话缓存的信息Evict和其他方法可以在NHibernate 文档的第 14.5 章中找到。

于 2009-07-30T07:31:45.427 回答
3

禁用延迟加载将强制返回实际实例而不是 NHibernate 代理。

例如..

映射.Not.LazyLoad();

或者

<class name="OrderLine" table="OrderLine" lazy="false" >
于 2011-03-13T21:11:03.387 回答
-2

由于代理是从实体类派生的,您可能只需检查 entity.GetType().BaseType 即可获取您定义的类型。

于 2009-07-30T03:31:22.240 回答