views:

460

answers:

6

I am wondering under what circumstances the following NHibernate code could fail:

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

The Assert statement always fails.

If I manually call session.Flush after SaveOrUpdate, then the select query succeeds, however I thought that we did not have to manually call flush? It was my understanding that NHibernate should be smart enough to realise that Foo has been updated, so the second select query should succeed.

Watching the SQL that is generated, it appears the second select query's SQL is executed before the first SaveOrUpdate's sql.

In fact, if I wrap the entire method in a transaction, then it succeeds:

using(NHibernateSessionManager.CurrentSession.BeginTransaction()
{
    // Same code as above
}

Now the SaveOrUpdate's sql will execute before the Linq.Where sql. This is a little strange, as I do not have to even commit the transaction in between.

What is going on?

+3  A: 

I recommend you leverage NHibernate transactions. It's entirely possible that, without their use, NHibernate has no way of determining when to issue your SaveOrUpdate call.

You'll find that even read-only statements perform better when using transactions. Please see http://nhprof.com/Learn/Alert?name=DoNotUseImplicitTransactions for further details.

For example:

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();
  }
}
David Andres
Interesting that adding a transaction works, even if I don't commit after calling SaveOrUpdate. Somehow, adding the transaction changes how NHibernate behaves, which I find a bit strange. However, I am already using TransactionScope (as this is a legacy app), which means that I can't use NHibernate BeginTransaction (at least, I have encountered all types of problems if I do try and use it). However, with a TransactionScope, the Assert fails. Seems like I need a way to make NHibernate behave as if I had called BeginTransaction, but to use the TransactionScope instead.
cbp
@cbp: That's disconcerting. Be careful with using a transaction without making a call to its Commit method. The transaction will be implicitly rolled back. This article: http://forum.springframework.net/showthread.php?t=5351, may help you. It deals with integrating TransactionScope and ITransaction. Incidentally, do you use a profiling tool or inspect the sql log file created by the NHibernate engine? If so, either the save/update query isn't happening when you expect it to or there are issues with NHibernate's first-level caching mechanism. I suspect the former is the case.
David Andres
@cbp: I meant to say, if you are looking at the sql log, you can determine exactly when statements are being executed against the database. As you can see, it's not painfully obvious when this actually happens.
David Andres
@David, yes I am watching the generated SQL by using the ShowSql configuration method. When all my code is wrapped in an NHibernate transaction, the SaveOrUpdate method causes the Insert method to execute before the select query. When the code is not wrapped in a transaction, the select query's sql executes before the Insert statement has had a chance to commit.
cbp
@cbp: Then no doubt the problem is with TransactionScope and ITransaction working together. Well, this other post http://stackoverflow.com/questions/1279926/nhibernate-first-level-cache-and-transaction-management-between-transactionscope has similar issues with first-level caching as you do. Sorry to make you read so much, but I honestly don't have an answer for you.
David Andres
A: 

You've probably got the incorrect flushmode set. You need to set the flushmode on the session to Auto for it to automatically flush the session before every query otherwise you need to flush the session manually to force the changes to be saved.

lomaxx
FlushMode is the default, which I believe is Auto?
cbp
A: 

If I manually call session.Flush after SaveOrUpdate, then the select query succeeds.

For a start: your call to SaveOrUpdate() is not even needed.

Here are some things to remember when using an NH session:

  • When you have Loaded an object from session, the session will continue to track changes to that object
  • Calling session.Update(entity) only tells the NHibernate session that it should start tracking the object, it does not go and write changes to the db

So, in your case, because you have loaded an object from the session already, calling session.Update() does nothing since it is already being tracked. You could infact force an update to the database by merely doing the following:

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);
Brendan Kowitz
Like I said, yes, I can call Flush manually and the statement succeeds. However, it seems that this is not the way to use NHibernate: it should not be necessary to manually call Flush everywhere - see my question here: http://stackoverflow.com/questions/1443214/flushing-in-nhibernate
cbp
Nice, Stefan in the other post has said exactly what I've said anyway. You asked "What is going on?" not "Why do I need to call Flush()?". Maybe re-read http://www.nhforge.org/doc/nh/en/index.html#manipulatingdata-flushing to gather your thoughts.
Brendan Kowitz
OK, the issue is not so much flushing. The issue is that NHibernate does not pick up the updated foo.SomeProperty, so the second session.Linq.Where query returns 0 records.
cbp
A: 

You have to close the session and create an now one before the Assert.

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();
  }
}
Kris-I
If I'm going to do all this, isn't it easier just to call Flush? I thought that NHibernate should be smart enough to work out that the entity has been updated.
cbp
If you tried the flush and try other flush method .... Me I use this method in my tests
Kris-I
+3  A: 

Note that you need a transaction for NHibernate to be "smart."

Here is how it works:

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

Note also that you do not call Save, Update, or SaveOrUpdate when you want to save the changes you have made to an object that the Session is already tracking back down to the database. NHibernate works differently from other ORMs: if it is tracking an object, then it will figure out when to send down the changes to the database and you don't need to tell it to do so.

Justice
+1  A: 

"I am wondering under what circumstances the following NHibernate code could fail:" I think you have supplied at least one answer to your own question: when the code is run inside an implicit transaction. See this post from Ayende, which mentions inconsistent behavior inside implicit transactions. I have many unit tests that resemble your code except the test driver supplies a wrapping transaction, e.g.,

[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<>() is a thin wrapper over Linq<>.] I have many such tests that pass regularly.

Keith Morgan