NHibernate没有捕捉到更改。

3
我想知道以下NHibernate代码在什么情况下可能会失败:
var session = NHibernateSessionManager.CurrentSession;

var foo = session.Linq<Foo>.ToList()[0];

foo.SomeProperty = "test";

session.SaveOrUpdate(foo);

var reloadedFoos = session.Linq<Foo>
                         .Where(x => x.SomeProperty == "test");

Assert.That(reloadedFoos.Count > 0);

Assert语句总是失败。
如果我在SaveOrUpdate之后手动调用session.Flush,那么选择查询会成功,但我认为我们不必手动调用flush?我的理解是NHibernate应该足够聪明,意识到Foo已经更新,因此第二个选择查询应该成功。
观察生成的SQL,似乎第二个选择查询的SQL在第一个SaveOrUpdate的SQL之前执行。
事实上,如果我将整个方法包装在一个事务中,那么它就成功了:
using(NHibernateSessionManager.CurrentSession.BeginTransaction()
{
    // Same code as above
}

现在SaveOrUpdate的SQL语句会在Linq.Where之前执行。这有点奇怪,因为我甚至不需要在两者之间提交事务。

到底发生了什么?


我认为你的问题有误导性。我感觉你想了解关于NHibernate工作单元模式的内容,涉及到与数据库同步更改的问题。你能否澄清一下? - Brendan Kowitz
我真正想要的是上述测试成功:我希望能够以可靠、简单的方式保存和重新加载Foo。 - cbp
6个回答

3

我建议您利用NHibernate事务。如果不使用它们,NHibernate可能无法确定何时发出您的SaveOrUpdate调用。

即使是只读语句,在使用事务时也会表现得更好。请参见http://nhprof.com/Learn/Alert?name=DoNotUseImplicitTransactions以获取更多详细信息。

例如:

using(var session = NHibernateSessionManager.CurrentSession)
{
  using(var transaction = session.BeginTransaction())
  {
    var foo = session.Linq<Foo>.ToList()[0];

    foo.SomeProperty = "test";

    session.SaveOrUpdate(foo);
    transaction.Commit();
  }
}

有趣的是,即使在调用SaveOrUpdate后不提交,添加事务也能起作用。不知何故,添加事务会改变NHibernate的行为,这让我感到有些奇怪。然而,由于这是一个遗留应用程序,我已经在使用TransactionScope,这意味着我不能使用NHibernate BeginTransaction(至少,如果我尝试使用它,我就会遇到各种问题)。但是,在使用TransactionScope时,Assert失败了。看来我需要一种方法,让NHibernate表现得好像我已经调用了BeginTransaction,但实际上使用TransactionScope。 - cbp
@cbp:这很令人不安。在使用事务时,请注意调用其Commit方法。否则,事务将被隐式回滚。这篇文章:http://forum.springframework.net/showthread.php?t=5351,可能会对您有所帮助。它涉及到集成TransactionScope和ITransaction。顺便问一下,您是否使用分析工具或检查NHibernate引擎创建的SQL日志文件?如果是这样,要么保存/更新查询没有在您期望的时间发生,要么NHibernate的一级缓存机制存在问题。我怀疑前者是原因。 - David Andres
@cbp:我的意思是,如果你查看SQL日志,你可以确定语句何时被执行到数据库中。正如你所看到的,这并不是非常明显的事情。 - David Andres
@David,是的,我正在使用ShowSql配置方法来观察生成的SQL语句。当我的所有代码都被包含在NHibernate事务中时,SaveOrUpdate方法会导致Insert方法在选择查询之前执行。当代码没有被包含在事务中时,选择查询的SQL语句会在插入语句有机会提交之前执行。 - cbp
1
@cbp:那么毫无疑问,问题出在TransactionScope和ITransaction一起使用上。嗯,这篇其他的帖子http://stackoverflow.com/questions/1279926/nhibernate-first-level-cache-and-transaction-management-between-transactionscope 也有与你类似的一级缓存问题。很抱歉让你读了这么多,但我真的没有答案可以给你。 - David Andres

3
请注意,NHibernate需要事务才能变得“聪明”。
以下是其工作原理:
var session = NHibernateSessionManager.CurrentSession;
using(NHibernateSessionManager.CurrentSession.BeginTransaction()) {
    var foo = session.Linq<Foo>.ToList()[0];
    foo.SomeProperty = "test";
    var reloadedFoos = session.Linq<Foo>()
        .Where(x => x.SomeProperty == "test");
    Assert.That(reloadedFoos.Count > 0);
}

请注意,当您想要保存已经被Session跟踪的对象所做的更改时,不需要调用SaveUpdateSaveOrUpdate。NHibernate与其他ORM工具的工作方式不同:如果它正在跟踪一个对象,那么它会自动判断何时将更改发送到数据库,您不需要告诉它这样做。请注意,不要删除标签和HTML标签。


1

"我在想以下NHibernate代码可能在什么情况下会失败:" 我认为你已经提供了至少一个答案:当代码在隐式事务内运行时。参见Ayende的此篇文章,其中提到在隐式事务内有不一致的行为。我有很多单元测试类似于您的代码,除了测试驱动程序提供了包装事务,例如,

[Test]
public void Can_Update_Account() {
        Account account = PersistenceContext.Get<Account>(TEST_ACCOUNT_ID);

        string accountNumber = RandomString(10);
        account.AccountNumber = accountNumber;

        Account account1 = PersistenceContext.GetAll<Account>().Where(x => x.AccountNumber == accountNumber).SingleOrDefault();
        Account account2 = PersistenceContext.Get<Account>(account.Id);
        Assert.AreEqual(account.Id, account1.Id);
        Assert.AreEqual(accountNumber, account2.AccountNumber);
    }

[GetAll<>()是Linq<>的一个薄包装器。] 我有许多这样的测试定期通过。


0
如果我在SaveOrUpdate之后手动调用session.Flush,那么选择查询就会成功。
首先:您甚至不需要调用SaveOrUpdate()。
这是使用NH session时需要记住的一些事情:
  • 当您从session中加载了一个对象时,session将继续跟踪该对象的更改
  • 调用session.Update(entity)仅告诉NHibernate session它应该开始跟踪对象,而不是写入更改到数据库
所以,在您的情况下,因为您已经从session中加载了一个对象,调用session.Update()什么也不做,因为它已经被跟踪了。实际上,您可以通过仅执行以下操作来强制更新数据库:
var session = NHibernateSessionManager.CurrentSession;
var foo = session.Linq<Foo>.ToList()[0];
foo.SomeProperty = "test";

session.Flush();

var reloadedFoos = session.Linq<Foo>
                         .Where(x => x.SomeProperty == "test");
Assert.That(reloadedFoos.Count > 0);

就像我说的那样,是的,我可以手动调用Flush并且语句会成功执行。然而,似乎这不是使用NHibernate的正确方法:不应该需要在每个地方手动调用Flush——请参见我的问题:http://stackoverflow.com/questions/1443214/flushing-in-nhibernate - cbp
很好,Stefan在另一篇帖子中已经说了我想说的话。你问的是"What is going on?"而不是"Why do I need to call Flush()?"。也许你需要重新阅读http://www.nhforge.org/doc/nh/en/index.html#manipulatingdata-flushing来整理一下你的思路。 - Brendan Kowitz
好的,问题并不是刷新。问题在于NHibernate没有捕获更新的foo.SomeProperty,因此第二个session.Linq.Where 查询返回0个记录。 - cbp

0

在进行断言之前,您必须关闭当前会话并创建一个新的会话。

using(var session = NHibernateSessionManager.CurrentSession)
{
  using(var tx = session.BeginTransaction())
  {
    var foo = session.Linq<Foo>.ToList()[0];
    foo.SomeProperty = "test";
    session.SaveOrUpdate(foo);  
    tx.Commit();
  }
}

//create a new session here, the code depend if you use RhinoCommons (like me), no Rhino

using(var session = NHibernateSessionManager.CurrentSession)
{
  using(var tx = session.BeginTransaction())
  {
    var reloadedFoos = session.Linq<Foo>
            .Where(x => x.SomeProperty == "test");
    Assert.That(reloadedFoos.Count > 0);
    tx.Commit();
  }
}

如果我要做所有这些,难道不直接调用Flush更容易吗?我认为NHibernate应该足够聪明,能够发现实体已经被更新了。 - cbp
如果您尝试了flush并尝试其他flush方法......我在我的测试中使用此方法。 - TheBoubou

0
你可能设置了错误的刷新模式。你需要在会话上设置自动刷新模式,以便在每次查询之前自动刷新会话,否则你需要手动刷新会话以强制保存更改。

FlushMode 是默认值,我认为是自动吗? - cbp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接